From 59e531ca2830f2c44aed6b8a1831f201303ea6d2 Mon Sep 17 00:00:00 2001 From: Caleb LeNoir Date: Mon, 23 Feb 2026 11:08:27 -0500 Subject: [PATCH 01/10] Add user-controlled dark/light mode toggle on account settings Adds a theme preference system allowing users to choose between light mode, dark mode, or system (OS) preference. New `theme` column on users table stores the preference, theme selector appears on account settings page, and a Stimulus controller manages dynamic theme switching. CSS media queries replaced with data-theme attribute selectors for consistent control. Co-Authored-By: Claude Haiku 4.5 --- app/assets/stylesheets/global.scss | 14 +- app/assets/stylesheets/home.scss | 4 +- app/assets/stylesheets/mixins.scss | 24 +-- app/assets/stylesheets/sessions/index.scss | 2 +- app/assets/stylesheets/sessions/sessions.scss | 14 +- app/assets/stylesheets/user/index.scss | 38 ++-- app/controllers/application_controller.rb | 5 + .../controllers/theme_controller.js | 28 +++ app/models/user.rb | 2 + app/views/devise/registrations/edit.html.erb | 18 ++ app/views/layouts/application.html.erb | 4 +- .../20260223160211_add_theme_to_users.rb | 5 + db/schema.rb | 181 +++++++++--------- 13 files changed, 204 insertions(+), 135 deletions(-) create mode 100644 app/javascript/controllers/theme_controller.js create mode 100644 db/migrate/20260223160211_add_theme_to_users.rb diff --git a/app/assets/stylesheets/global.scss b/app/assets/stylesheets/global.scss index df7f93d..836b887 100644 --- a/app/assets/stylesheets/global.scss +++ b/app/assets/stylesheets/global.scss @@ -95,8 +95,8 @@ button, input[type=submit] { border-radius: $border-radius; padding: 8px 16px; cursor: pointer; - - @media (prefers-color-scheme: dark) { + + [data-theme="dark"] & { color: $white; border: 1px solid $light; @@ -104,7 +104,7 @@ button, input[type=submit] { border-color: $white; } } - + } input[type=text] { @@ -114,7 +114,7 @@ input[type=text] { border-bottom: 1px solid $dark; outline: none; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { color: $light; border-bottom: 1px solid $light; } @@ -130,7 +130,7 @@ textarea { padding: 0 8px; resize: none; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { color: $light; } } @@ -139,9 +139,7 @@ textarea { display: none; } -@media (prefers-color-scheme: dark) { - //transition: all 1s; - +[data-theme="dark"] { body { background: $dark; color: $light; diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss index f9512fb..d448518 100644 --- a/app/assets/stylesheets/home.scss +++ b/app/assets/stylesheets/home.scss @@ -20,7 +20,7 @@ border: 1px solid $dark; padding: 10px; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { border: 1px solid $light; } @@ -60,7 +60,7 @@ } } - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { .primary-link, .secondary-link { border-color: lightgray; diff --git a/app/assets/stylesheets/mixins.scss b/app/assets/stylesheets/mixins.scss index 22b679b..656e5c6 100644 --- a/app/assets/stylesheets/mixins.scss +++ b/app/assets/stylesheets/mixins.scss @@ -34,26 +34,22 @@ } @mixin logo-darkmode { - @media (prefers-color-scheme: dark) { - .logo { - &.light { - display: none; - } - &.dark { - display: block; - } + .logo { + &.light { + display: block; + } + &.dark { + display: none; } - - transition: ease all 1s; } - @media (prefers-color-scheme: light) { + [data-theme="dark"] & { .logo { &.light { - display: block; + display: none; } &.dark { - display: none; + display: block; } } @@ -84,7 +80,7 @@ } } - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { textarea { background: #202020; color: lightgray; diff --git a/app/assets/stylesheets/sessions/index.scss b/app/assets/stylesheets/sessions/index.scss index 9f9facf..3dff913 100644 --- a/app/assets/stylesheets/sessions/index.scss +++ b/app/assets/stylesheets/sessions/index.scss @@ -29,7 +29,7 @@ } } - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { .primary-link, .secondary-link { border-color: lightgray; diff --git a/app/assets/stylesheets/sessions/sessions.scss b/app/assets/stylesheets/sessions/sessions.scss index eedc8bc..ba72c9b 100644 --- a/app/assets/stylesheets/sessions/sessions.scss +++ b/app/assets/stylesheets/sessions/sessions.scss @@ -44,10 +44,9 @@ opacity: 1; } - @media (prefers-color-scheme: light) { - color: $dark; - } - @media (prefers-color-scheme: dark) { + color: $dark; + + [data-theme="dark"] & { color: $light; } @@ -127,7 +126,7 @@ border-radius: $border-radius; max-height: 100px; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { border: 1px solid $border-color-dark; } @@ -162,7 +161,7 @@ justify-content: flex-start; border-left: 1px solid $border-color-dark; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { border-left: 1px solid $border-color-dark; } @@ -181,9 +180,8 @@ background: black; color: white; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { background: rgba(255, 255, 255, 0.4); - // color: black; } } } diff --git a/app/assets/stylesheets/user/index.scss b/app/assets/stylesheets/user/index.scss index 53d991f..bf529d5 100644 --- a/app/assets/stylesheets/user/index.scss +++ b/app/assets/stylesheets/user/index.scss @@ -39,18 +39,36 @@ opacity: 1; } - @media (prefers-color-scheme: light) { - border: 1px solid black; - color: black; - } + border: 1px solid black; + color: black; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { border: 1px solid white; color: white; } } } + .theme-options { + display: flex; + gap: 16px; + margin-top: 5px; + + label { + display: flex; + align-items: center; + gap: 4px; + cursor: pointer; + } + + input[type="radio"] { + width: auto; + margin: 0; + padding: 0; + border: none; + } + } + .actions { input[type="submit"] { width: 100%; @@ -65,13 +83,11 @@ opacity: 1; } - @media (prefers-color-scheme: light) { - border: 1px solid black; - color: white; - background: black; - } + border: 1px solid black; + color: white; + background: black; - @media (prefers-color-scheme: dark) { + [data-theme="dark"] & { border: 1px solid white; color: black; background: white; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 857932b..49ab438 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,8 +1,13 @@ class ApplicationController < ActionController::Base before_action :authenticate_user! + before_action :configure_permitted_parameters, if: :devise_controller? protected + def configure_permitted_parameters + devise_parameter_sanitizer.permit(:account_update, keys: [:theme]) + end + rescue_from CanCan::AccessDenied do |_exception| redirect_to root_path, flash: { error: 'You are not authorized to perform this action.' } end diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js new file mode 100644 index 0000000..2368858 --- /dev/null +++ b/app/javascript/controllers/theme_controller.js @@ -0,0 +1,28 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + connect() { + this.mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + this.handleSystemChange = this.handleSystemChange.bind(this) + + if (this.element.dataset.themePreference === "system") { + this.applySystemTheme() + this.mediaQuery.addEventListener("change", this.handleSystemChange) + } + } + + disconnect() { + this.mediaQuery.removeEventListener("change", this.handleSystemChange) + } + + handleSystemChange() { + if (this.element.dataset.themePreference === "system") { + this.applySystemTheme() + } + } + + applySystemTheme() { + const isDark = this.mediaQuery.matches + this.element.dataset.theme = isDark ? "dark" : "light" + } +} diff --git a/app/models/user.rb b/app/models/user.rb index 260c72e..cc5dda8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -7,6 +7,8 @@ class User < ApplicationRecord has_many :writing_sessions, dependent: :destroy has_many :stories, dependent: :destroy + validates :theme, inclusion: { in: %w[light dark system] } + after_create :subscribe_to_mailing def password_required? diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 64146c5..aa21daa 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -12,6 +12,24 @@
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
<% end %> +
+ <%= f.label :theme, "Theme" %>
+
+ <%= f.label :theme_light do %> + <%= f.radio_button :theme, "light" %> + Light + <% end %> + <%= f.label :theme_dark do %> + <%= f.radio_button :theme, "dark" %> + Dark + <% end %> + <%= f.label :theme_system do %> + <%= f.radio_button :theme, "system" %> + System + <% end %> +
+
+
<%= f.label :password %> (leave blank if you don't want to change it)
<%= f.password_field :password, autocomplete: "new-password", placeholder: "New Password" %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c0187bb..0208b9b 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,7 @@ - +<% theme_pref = current_user&.theme || "system" %> +<% resolved_theme = theme_pref == "system" ? nil : theme_pref %> +> <%= @title || 'Draft' %> diff --git a/db/migrate/20260223160211_add_theme_to_users.rb b/db/migrate/20260223160211_add_theme_to_users.rb new file mode 100644 index 0000000..1e7f7f1 --- /dev/null +++ b/db/migrate/20260223160211_add_theme_to_users.rb @@ -0,0 +1,5 @@ +class AddThemeToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :theme, :string, default: "system", null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index d6dd202..cc98218 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,114 +10,115 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 20_250_612_164_159) do - create_table 'blazer_audits', force: :cascade do |t| - t.integer 'user_id' - t.integer 'query_id' - t.text 'statement' - t.string 'data_source' - t.datetime 'created_at', precision: nil - t.index ['query_id'], name: 'index_blazer_audits_on_query_id' - t.index ['user_id'], name: 'index_blazer_audits_on_user_id' +ActiveRecord::Schema[8.0].define(version: 2026_02_23_160211) do + create_table "blazer_audits", force: :cascade do |t| + t.integer "user_id" + t.integer "query_id" + t.text "statement" + t.string "data_source" + t.datetime "created_at", precision: nil + t.index ["query_id"], name: "index_blazer_audits_on_query_id" + t.index ["user_id"], name: "index_blazer_audits_on_user_id" end - create_table 'blazer_checks', force: :cascade do |t| - t.integer 'creator_id' - t.integer 'query_id' - t.string 'state' - t.string 'schedule' - t.text 'emails' - t.text 'slack_channels' - t.string 'check_type' - t.text 'message' - t.datetime 'last_run_at', precision: nil - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['creator_id'], name: 'index_blazer_checks_on_creator_id' - t.index ['query_id'], name: 'index_blazer_checks_on_query_id' + create_table "blazer_checks", force: :cascade do |t| + t.integer "creator_id" + t.integer "query_id" + t.string "state" + t.string "schedule" + t.text "emails" + t.text "slack_channels" + t.string "check_type" + t.text "message" + t.datetime "last_run_at", precision: nil + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_checks_on_creator_id" + t.index ["query_id"], name: "index_blazer_checks_on_query_id" end - create_table 'blazer_dashboard_queries', force: :cascade do |t| - t.integer 'dashboard_id' - t.integer 'query_id' - t.integer 'position' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['dashboard_id'], name: 'index_blazer_dashboard_queries_on_dashboard_id' - t.index ['query_id'], name: 'index_blazer_dashboard_queries_on_query_id' + create_table "blazer_dashboard_queries", force: :cascade do |t| + t.integer "dashboard_id" + t.integer "query_id" + t.integer "position" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["dashboard_id"], name: "index_blazer_dashboard_queries_on_dashboard_id" + t.index ["query_id"], name: "index_blazer_dashboard_queries_on_query_id" end - create_table 'blazer_dashboards', force: :cascade do |t| - t.integer 'creator_id' - t.string 'name' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['creator_id'], name: 'index_blazer_dashboards_on_creator_id' + create_table "blazer_dashboards", force: :cascade do |t| + t.integer "creator_id" + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_dashboards_on_creator_id" end - create_table 'blazer_queries', force: :cascade do |t| - t.integer 'creator_id' - t.string 'name' - t.text 'description' - t.text 'statement' - t.string 'data_source' - t.string 'status' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['creator_id'], name: 'index_blazer_queries_on_creator_id' + create_table "blazer_queries", force: :cascade do |t| + t.integer "creator_id" + t.string "name" + t.text "description" + t.text "statement" + t.string "data_source" + t.string "status" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["creator_id"], name: "index_blazer_queries_on_creator_id" end - create_table 'outline_items', force: :cascade do |t| - t.integer 'outline_id' - t.string 'text' - t.boolean 'completed', default: false - t.integer 'position' - t.string 'timestamps' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['outline_id'], name: 'index_outline_items_on_outline_id' + create_table "outline_items", force: :cascade do |t| + t.integer "outline_id" + t.string "text" + t.boolean "completed", default: false + t.integer "position" + t.string "timestamps" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["outline_id"], name: "index_outline_items_on_outline_id" end - create_table 'outlines', force: :cascade do |t| - t.integer 'story_id' - t.integer 'completion' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['story_id'], name: 'index_outlines_on_story_id' + create_table "outlines", force: :cascade do |t| + t.integer "story_id" + t.integer "completion" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["story_id"], name: "index_outlines_on_story_id" end - create_table 'stories', force: :cascade do |t| - t.string 'title', null: false - t.integer 'user_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['user_id'], name: 'index_stories_on_user_id' + create_table "stories", force: :cascade do |t| + t.string "title", null: false + t.integer "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_stories_on_user_id" end - create_table 'users', force: :cascade do |t| - t.string 'email', default: '', null: false - t.string 'encrypted_password', default: '', null: false - t.string 'reset_password_token' - t.datetime 'reset_password_sent_at', precision: nil - t.datetime 'remember_created_at', precision: nil - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.boolean 'admin_role' - t.index ['email'], name: 'index_users_on_email', unique: true - t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true + create_table "users", force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at", precision: nil + t.datetime "remember_created_at", precision: nil + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "admin_role" + t.string "theme", default: "system", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end - create_table 'writing_sessions', force: :cascade do |t| - t.integer 'word_count' - t.text 'text' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'user_id' - t.integer 'story_id', null: false - t.index ['story_id'], name: 'index_writing_sessions_on_story_id' - t.index ['user_id'], name: 'index_writing_sessions_on_user_id' + create_table "writing_sessions", force: :cascade do |t| + t.integer "word_count" + t.text "text" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "user_id" + t.integer "story_id", null: false + t.index ["story_id"], name: "index_writing_sessions_on_story_id" + t.index ["user_id"], name: "index_writing_sessions_on_user_id" end - add_foreign_key 'writing_sessions', 'stories' - add_foreign_key 'writing_sessions', 'users' + add_foreign_key "writing_sessions", "stories" + add_foreign_key "writing_sessions", "users" end From 868f4320520eb3f4b7799f77a98f534d632d142f Mon Sep 17 00:00:00 2001 From: Caleb LeNoir Date: Mon, 23 Feb 2026 12:21:28 -0500 Subject: [PATCH 02/10] Fix Turbo form error by allowing password-less account updates Devise requires current_password for all updates, which causes a re-render with 200 status that Turbo rejects. Custom registrations controller skips password validation when only non-sensitive fields like theme are being changed. Co-Authored-By: Claude Opus 4.6 --- .../users/registrations_controller.rb | 18 ++++++++++++++++++ config/routes.rb | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 app/controllers/users/registrations_controller.rb diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 0000000..9d47f3c --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,18 @@ +class Users::RegistrationsController < Devise::RegistrationsController + protected + + def update_resource(resource, params) + if params[:password].blank? && params[:password_confirmation].blank? + params.delete(:password) + params.delete(:password_confirmation) + params.delete(:current_password) + resource.update(params) + else + super + end + end + + def after_update_path_for(resource) + edit_user_registration_path + end +end diff --git a/config/routes.rb b/config/routes.rb index 43ca22a..fd72da6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ Rails.application.routes.draw do mount Blazer::Engine, at: 'blazer' - devise_for :user + devise_for :user, controllers: { registrations: 'users/registrations' } resources :stories do resources :writing_sessions From ec4aacf7f50360e0b13238a1eff776cbd9969a3b Mon Sep 17 00:00:00 2001 From: Caleb LeNoir Date: Mon, 23 Feb 2026 12:27:28 -0500 Subject: [PATCH 03/10] Import toastr in application.js and expose as window global The inline flash message script expects toastr as a global, but it was only pinned in the importmap without being imported. This causes a ReferenceError during Turbo page navigations. Co-Authored-By: Claude Opus 4.6 --- app/javascript/application.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/javascript/application.js b/app/javascript/application.js index 7ca6576..12fff96 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -3,4 +3,7 @@ import '@hotwired/turbo-rails' import 'jquery' import 'sortablejs' -import 'controllers' \ No newline at end of file +import toastr from 'toastr' +import 'controllers' + +window.toastr = toastr \ No newline at end of file From dcb40872e7069adb2be3a699145e9257eb6405d5 Mon Sep 17 00:00:00 2001 From: Caleb LeNoir Date: Mon, 23 Feb 2026 12:31:20 -0500 Subject: [PATCH 04/10] Move theme attributes from html to body for Turbo compatibility Turbo replaces the body on navigation but does not update html element attributes. Moving data-theme and the Stimulus controller to body ensures theme is applied after Turbo page transitions. Co-Authored-By: Claude Opus 4.6 --- app/assets/stylesheets/global.scss | 10 ++++------ app/views/layouts/application.html.erb | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/global.scss b/app/assets/stylesheets/global.scss index 836b887..8f2fa35 100644 --- a/app/assets/stylesheets/global.scss +++ b/app/assets/stylesheets/global.scss @@ -139,12 +139,10 @@ textarea { display: none; } -[data-theme="dark"] { - body { - background: $dark; - color: $light; - transition: ease all 1s; - } +body[data-theme="dark"] { + background: $dark; + color: $light; + transition: ease all 1s; a { color: $light; diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 0208b9b..f7c9938 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,7 +1,5 @@ -<% theme_pref = current_user&.theme || "system" %> -<% resolved_theme = theme_pref == "system" ? nil : theme_pref %> -> + <%= @title || 'Draft' %> @@ -15,7 +13,9 @@ <%= javascript_importmap_tags %> - +<% theme_pref = current_user&.theme || "system" %> +<% resolved_theme = theme_pref == "system" ? nil : theme_pref %> +> <% unless flash.empty? %> <% unless flash.empty? %> - +<% theme_pref = current_user&.theme || "system" %> + + <% unless flash.empty? %> - +<% theme_pref = current_user&.theme || "system" %> + + <% unless flash.empty? %> + + <%= render "shared/theme" %> <% unless flash.empty? %> -<% theme_pref = current_user&.theme || "system" %> - - + + <%= render "shared/theme" %> <% unless flash.empty? %> -<% theme_pref = current_user&.theme || "system" %> - - + + <%= render "shared/theme" %> <% unless flash.empty? %> From a93b8b51315d03dc6102730d3ea136164d4b5501 Mon Sep 17 00:00:00 2001 From: Caleb LeNoir Date: Tue, 24 Feb 2026 11:38:38 -0500 Subject: [PATCH 10/10] Add theme toggle button and fix stale matchMedia listener Add a cycling theme toggle button (light/dark/system) to the home page header, theme API endpoint, and supporting styles. Move theme preference to a shared mutable variable (window.__themePref) so the matchMedia listener stays in sync after client-side toggles without a page reload. Co-Authored-By: Claude Opus 4.6 --- app/assets/stylesheets/home.scss | 23 ++++++++++ .../users/registrations_controller.rb | 11 ----- app/controllers/users/themes_controller.rb | 9 ++++ app/views/devise/registrations/edit.html.erb | 18 -------- app/views/home/index.html.erb | 45 +++++++++++++++++++ app/views/shared/_theme.html.erb | 3 +- config/routes.rb | 4 ++ 7 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 app/controllers/users/themes_controller.rb diff --git a/app/assets/stylesheets/home.scss b/app/assets/stylesheets/home.scss index d448518..f8f1075 100644 --- a/app/assets/stylesheets/home.scss +++ b/app/assets/stylesheets/home.scss @@ -79,5 +79,28 @@ margin-top: 20px; opacity: 0.4; } + + .theme-toggle { + border: none !important; + background: none; + padding: 4px 8px; + cursor: pointer; + color: gray; + font-size: 14px; + line-height: 30px; + + &:hover { + color: black; + background: lightgray; + border-radius: $border-radius; + } + + [data-theme="dark"] & { + &:hover { + color: white; + background: #202020; + } + } + } } diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 9d47f3c..0a7ce20 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,17 +1,6 @@ class Users::RegistrationsController < Devise::RegistrationsController protected - def update_resource(resource, params) - if params[:password].blank? && params[:password_confirmation].blank? - params.delete(:password) - params.delete(:password_confirmation) - params.delete(:current_password) - resource.update(params) - else - super - end - end - def after_update_path_for(resource) edit_user_registration_path end diff --git a/app/controllers/users/themes_controller.rb b/app/controllers/users/themes_controller.rb new file mode 100644 index 0000000..5f61c44 --- /dev/null +++ b/app/controllers/users/themes_controller.rb @@ -0,0 +1,9 @@ +class Users::ThemesController < ApplicationController + def update + new_theme = params[:theme] + if %w[light dark system].include?(new_theme) + current_user.update!(theme: new_theme) + end + head :ok + end +end diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index aa21daa..64146c5 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -12,24 +12,6 @@
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
<% end %> -
- <%= f.label :theme, "Theme" %>
-
- <%= f.label :theme_light do %> - <%= f.radio_button :theme, "light" %> - Light - <% end %> - <%= f.label :theme_dark do %> - <%= f.radio_button :theme, "dark" %> - Dark - <% end %> - <%= f.label :theme_system do %> - <%= f.radio_button :theme, "system" %> - System - <% end %> -
-
-
<%= f.label :password %> (leave blank if you don't want to change it)
<%= f.password_field :password, autocomplete: "new-password", placeholder: "New Password" %> diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index a150a3e..c66f04c 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -2,6 +2,11 @@

+ <%= link_to 'Account', edit_user_registration_path, class: 'header-action', data: {'turbo-action': :replace} %> <%= link_to 'Logout', destroy_user_session_path, method: 'DELETE', class: 'header-action', data: {'turbo-action': :restore, "turbo-method": :delete} %>
@@ -28,3 +33,43 @@
+ + diff --git a/app/views/shared/_theme.html.erb b/app/views/shared/_theme.html.erb index 1b2046a..0e014d7 100644 --- a/app/views/shared/_theme.html.erb +++ b/app/views/shared/_theme.html.erb @@ -1,12 +1,13 @@