Skip to content

Commit 0c95b2e

Browse files
authored
OTWO-7576 Preserve dark mode settings per user (#1894)
* OTWO-7576 Preserve dark mode settings per user * Code Review Fix * Removed extra code from sessions controller * Added testcase coverage
1 parent 1aea23b commit 0c95b2e

11 files changed

Lines changed: 564 additions & 40 deletions

File tree

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,42 @@
11
// Mobile Menu Toggle Functionality
22
var MobileMenu = {
33
init: function() {
4-
console.log('MobileMenu: Initializing...');
54
this.bindEvents();
65
},
76

87
toggleMenu: function() {
9-
console.log('MobileMenu: Toggle clicked!');
108
var mobileMenu = document.getElementById('mobile-menu');
119

1210
if (mobileMenu) {
1311
if (mobileMenu.classList.contains('show')) {
1412
mobileMenu.classList.remove('show');
15-
console.log('MobileMenu: Menu closed');
1613
} else {
1714
mobileMenu.classList.add('show');
18-
console.log('MobileMenu: Menu opened');
1915
}
20-
} else {
21-
console.log('MobileMenu: WARNING - Mobile menu element not found!');
2216
}
2317
},
2418

2519
closeMenu: function() {
2620
var mobileMenu = document.getElementById('mobile-menu');
2721
if (mobileMenu) {
2822
mobileMenu.classList.remove('show');
29-
console.log('MobileMenu: Menu closed');
3023
}
3124
},
3225

3326
bindEvents: function() {
3427
var self = this;
3528
var toggleBtn = document.getElementById('mobile-menu-toggle');
3629

37-
console.log('MobileMenu: Toggle button found?', !!toggleBtn);
38-
3930
if (toggleBtn) {
4031
toggleBtn.onclick = function(e) {
4132
e.preventDefault();
4233
self.toggleMenu();
4334
return false;
4435
};
45-
console.log('MobileMenu: Click handler attached');
46-
} else {
47-
console.log('MobileMenu: WARNING - Mobile menu toggle button not found!');
4836
}
4937

5038
// Close menu when clicking on a link
5139
var mobileMenuLinks = document.querySelectorAll('.mobile-menu-items a, .mobile-menu-user a, .mobile-menu-signin a');
52-
console.log('MobileMenu: Found', mobileMenuLinks.length, 'menu links');
5340

5441
for (var i = 0; i < mobileMenuLinks.length; i++) {
5542
mobileMenuLinks[i].onclick = function() {
@@ -69,18 +56,15 @@ var MobileMenu = {
6956
// Initialize on DOM ready
7057
if (typeof $ !== 'undefined') {
7158
$(document).on('page:change', function() {
72-
console.log('MobileMenu: page:change event fired');
7359
MobileMenu.init();
7460
});
7561

7662
$(document).ready(function() {
77-
console.log('MobileMenu: document.ready event fired');
7863
MobileMenu.init();
7964
});
8065
} else {
8166
// Fallback if jQuery is not available
8267
document.addEventListener('DOMContentLoaded', function() {
83-
console.log('MobileMenu: DOMContentLoaded event fired');
8468
MobileMenu.init();
8569
});
8670
}

app/assets/javascripts/theme_toggle.js

Lines changed: 128 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,134 @@
11
// Theme Toggle Functionality
22
var ThemeToggle = {
3+
COOKIE_NAME: 'theme_preference',
4+
COOKIE_DAYS: 365,
5+
36
init: function() {
4-
console.log('ThemeToggle: Initializing...');
5-
var savedTheme = this.getSavedTheme();
6-
console.log('ThemeToggle: Saved theme is', savedTheme);
7-
this.applyTheme(savedTheme);
8-
this.bindEvents();
7+
var self = this;
8+
this.isAuthenticated = this.checkAuthentication();
9+
10+
if (this.isAuthenticated) {
11+
this.loadServerThemePreference(function(theme) {
12+
self.applyTheme(theme);
13+
self.bindEvents();
14+
});
15+
} else {
16+
var savedTheme = this.getSavedTheme();
17+
this.applyTheme(savedTheme);
18+
this.bindEvents();
19+
}
20+
},
21+
22+
checkAuthentication: function() {
23+
var metaTag = document.querySelector('meta[name="current-user"]');
24+
return metaTag && metaTag.getAttribute('content');
25+
},
26+
27+
getCurrentUserId: function() {
28+
var metaTag = document.querySelector('meta[name="current-user"]');
29+
return metaTag ? metaTag.getAttribute('content') : null;
30+
},
31+
32+
loadServerThemePreference: function(callback) {
33+
var userId = this.getCurrentUserId();
34+
if (!userId) {
35+
callback(this.getSystemTheme());
36+
return;
37+
}
38+
39+
var self = this;
40+
fetch('/accounts/' + userId + '/theme_preference.json', {
41+
method: 'GET',
42+
credentials: 'same-origin',
43+
headers: {
44+
'Accept': 'application/json'
45+
}
46+
})
47+
.then(function(response) {
48+
if (!response.ok) {
49+
return callback(self.getSystemTheme());
50+
}
51+
return response.json();
52+
})
53+
.then(function(data) {
54+
if (data && data.theme_preference) {
55+
callback(data.theme_preference);
56+
} else {
57+
callback(self.getSystemTheme());
58+
}
59+
})
60+
.catch(function(error) {
61+
callback(self.getSystemTheme());
62+
});
63+
},
64+
65+
saveServerThemePreference: function(theme) {
66+
if (!this.isAuthenticated) {
67+
return;
68+
}
69+
70+
var userId = this.getCurrentUserId();
71+
if (!userId) {
72+
return;
73+
}
74+
75+
fetch('/accounts/' + userId + '/set_theme_preference', {
76+
method: 'POST',
77+
credentials: 'same-origin',
78+
headers: {
79+
'Content-Type': 'application/json',
80+
'X-CSRF-Token': this.getCsrfToken()
81+
},
82+
body: JSON.stringify({ theme: theme })
83+
})
84+
.then(function(response) {
85+
return response.json();
86+
})
87+
.catch(function(error) {
88+
// Silent fail - cookie already set
89+
});
90+
},
91+
92+
getCsrfToken: function() {
93+
var token = document.querySelector('meta[name="csrf-token"]');
94+
return token ? token.getAttribute('content') : '';
95+
},
96+
97+
getCookie: function(name) {
98+
var nameEQ = name + '=';
99+
var cookies = document.cookie.split(';');
100+
for (var i = 0; i < cookies.length; i++) {
101+
var cookie = cookies[i].trim();
102+
if (cookie.indexOf(nameEQ) === 0) {
103+
return cookie.substring(nameEQ.length);
104+
}
105+
}
106+
return null;
107+
},
108+
109+
setCookie: function(name, value, days) {
110+
var expires = '';
111+
if (days) {
112+
var date = new Date();
113+
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
114+
expires = '; expires=' + date.toUTCString();
115+
}
116+
document.cookie = name + '=' + value + expires + '; path=/; SameSite=Lax';
117+
},
118+
119+
getSystemTheme: function() {
120+
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
121+
return 'dark';
122+
}
123+
return 'light';
9124
},
10125

11126
getSavedTheme: function() {
12-
try {
13-
return localStorage.getItem('theme') || 'light';
14-
} catch (e) {
15-
return 'light';
127+
var cookieTheme = this.getCookie(this.COOKIE_NAME);
128+
if (cookieTheme && (cookieTheme === 'light' || cookieTheme === 'dark')) {
129+
return cookieTheme;
16130
}
131+
return this.getSystemTheme();
17132
},
18133

19134
applyTheme: function(theme) {
@@ -31,11 +146,7 @@ var ThemeToggle = {
31146
if (sunIcon) sunIcon.classList.add('hidden');
32147
}
33148

34-
try {
35-
localStorage.setItem('theme', theme);
36-
} catch (e) {
37-
console.log('Could not save theme preference');
38-
}
149+
this.setCookie(this.COOKIE_NAME, theme, this.COOKIE_DAYS);
39150

40151
if (typeof Charts !== 'undefined') {
41152
Charts.updateWatermarks(theme === 'dark');
@@ -46,24 +157,21 @@ var ThemeToggle = {
46157
var currentTheme = this.getSavedTheme();
47158
var newTheme = currentTheme === 'light' ? 'dark' : 'light';
48159
this.applyTheme(newTheme);
160+
if (this.isAuthenticated) {
161+
this.saveServerThemePreference(newTheme);
162+
}
49163
},
50164

51165
bindEvents: function() {
52166
var self = this;
53167
var themeToggleBtn = document.getElementById('theme-toggle');
54168

55-
console.log('ThemeToggle: Theme toggle button found?', !!themeToggleBtn);
56-
57169
if (themeToggleBtn) {
58170
themeToggleBtn.onclick = function(e) {
59171
e.preventDefault();
60-
console.log('ThemeToggle: Toggle clicked!');
61172
self.toggleTheme();
62173
return false;
63174
};
64-
console.log('ThemeToggle: Click handler attached');
65-
} else {
66-
console.log('ThemeToggle: WARNING - Theme toggle button not found!');
67175
}
68176
}
69177
};

app/controllers/accounts_controller.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ class AccountsController < ApplicationController
88
helper MapHelper
99

1010
skip_before_action :store_location, only: %i[new create]
11-
before_action :session_required, only: %i[edit destroy confirm_delete me]
12-
before_action :set_account, only: %i[destroy show update edit confirm_delete disabled settings]
11+
before_action :session_required,
12+
only: %i[edit destroy confirm_delete me theme_preference set_theme_preference]
13+
before_action :set_account,
14+
only: %i[destroy show update edit confirm_delete disabled settings theme_preference
15+
set_theme_preference]
1316
before_action :redirect_if_disabled, only: %i[show update edit]
1417
before_action :redirect_unverified_account, only: %i[edit update me]
1518
before_action :disabled_during_read_only_mode, only: %i[edit update]
1619
before_action :account_context, only: %i[edit update confirm_delete]
17-
before_action :must_own_account, only: %i[edit update destroy confirm_delete]
20+
before_action :must_own_account, only: %i[edit update destroy confirm_delete theme_preference set_theme_preference]
1821
before_action :find_claimed_people, only: :index
1922
before_action :redirect_if_logged_in, only: :new
2023
before_action :check_honeypot, only: :create
@@ -91,6 +94,19 @@ def disabled; end
9194

9295
def settings; end
9396

97+
def theme_preference
98+
respond_to do |format|
99+
format.json { render json: { theme_preference: @account.theme_preference } }
100+
end
101+
end
102+
103+
def set_theme_preference
104+
@account.theme_preference = params[:theme]
105+
render json: { success: true, theme: @account.theme_preference }
106+
rescue ArgumentError
107+
render json: { error: 'Invalid theme preference' }, status: :unprocessable_entity
108+
end
109+
94110
private
95111

96112
def find_claimed_people

app/models/account.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ def project_core
135135
@project_core ||= Account::ProjectCore.new(self)
136136
end
137137

138+
def theme_preference
139+
setting = Setting.find_by(key: "account_#{id}_theme_preference")
140+
setting&.value
141+
end
142+
143+
def theme_preference=(theme)
144+
if theme == 'dark'
145+
Setting.find_or_create_by(key: "account_#{id}_theme_preference").update(value: 'dark')
146+
else
147+
Setting.find_by(key: "account_#{id}_theme_preference")&.destroy
148+
end
149+
end
150+
138151
class << self
139152
def resolve_login(login)
140153
Account.find_by('lower(login) = ?', login.to_s.downcase)

app/views/layouts/application.html.haml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,30 @@
22
%html
33
%head
44
:javascript
5-
try { if (localStorage.getItem('theme') === 'dark') document.documentElement.classList.add('dark'); } catch(e) {}
5+
(function() {
6+
var COOKIE_NAME = 'theme_preference';
7+
var getCookie = function(name) {
8+
var nameEQ = name + '=';
9+
var cookies = document.cookie.split(';');
10+
for (var i = 0; i < cookies.length; i++) {
11+
var cookie = cookies[i].trim();
12+
if (cookie.indexOf(nameEQ) === 0) {
13+
return cookie.substring(nameEQ.length);
14+
}
15+
}
16+
return null;
17+
};
18+
var cookieTheme = getCookie(COOKIE_NAME);
19+
var theme = 'light';
20+
if (cookieTheme && (cookieTheme === 'light' || cookieTheme === 'dark')) {
21+
theme = cookieTheme;
22+
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
23+
theme = 'dark';
24+
}
25+
if (theme === 'dark') {
26+
document.documentElement.classList.add('dark');
27+
}
28+
})();
629
- if Rails.env.production?
730
= render partial: 'layouts/tracking_scripts/google_analytics'
831
- page_title = content_for?(:html_title) ? yield(:html_title).to_s : t('.openhub')
@@ -17,6 +40,8 @@
1740
= yield :custom_head
1841
= stylesheet_link_tag 'application', media: 'all'
1942
= csrf_meta_tags
43+
- unless current_user.nil?
44+
%meta{ name: 'current-user', content: current_user.id }
2045

2146
%body{ zoom: 1 }
2247
= yield :session_projects_banner

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@
9898
get :confirm_delete
9999
get :disabled
100100
get :settings
101+
get :theme_preference, defaults: { format: :json }
102+
post :set_theme_preference
101103
get 'alter_password/edit', to: 'alter_passwords#edit'
102104
patch 'alter_password/edit', to: 'alter_passwords#update'
103105
get :edit_privacy, to: 'privacy#edit', as: :edit_account_privacy

0 commit comments

Comments
 (0)