diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2a1503d2e27d4c281241ce225ffc38cc73e4c184..dee25aaf142779313043a99beb28d2396f33bee9 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,25 @@ class UsersController < ApplicationController + before_action :authenticate_user!, only: [ :update_theme ] + def leaderboard @workload_data = User.leaderboard end + + def update_theme + if current_user.present? + # Ensure darkmode parameter is valid (light, dark) + theme = params[:darkmode].to_s + if [ "light", "dark" ].include?(theme) + if current_user.update(darkmode: theme) + render json: { success: true, theme: theme }, status: :ok + else + render json: { success: false, errors: current_user.errors.full_messages }, status: :unprocessable_entity + end + else + render json: { success: false, error: "Invalid theme value" }, status: :bad_request + end + else + render json: { success: false, error: "User not authenticated" }, status: :unauthorized + end + end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index e2deef9693d887f2b1b35a554600a0f6973c437e..561b9856dc18d66ff7dd742d1eda810a01550569 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -45,7 +45,7 @@ </div> </div> - <nav class="flex items-center"> + <nav class="flex items-center space-x-3"> <% if user_signed_in? %> <!-- Desktop user navigation --> <div class="hidden md:flex md:items-center md:space-x-4"> @@ -66,6 +66,16 @@ <div class="flex items-center ml-4"> <%= render partial: "application/user_avatar", locals: { user: current_user } %> </div> + + <!-- Dark mode toggle button --> + <button id="theme-toggle" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" aria-label="Toggle dark mode"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden dark:block" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /> + </svg> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 block dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /> + </svg> + </button> <!-- Mobile hamburger menu --> <div class="md:hidden relative ml-4"> @@ -100,10 +110,19 @@ <% else %> <!-- Not logged in state --> <div class="flex items-center space-x-4"> - <span class="px-2 text-gray-600 dark:text-gray-300 hidden md:inline">not logged in</span> <%= link_to "Assignments", assignments_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> <%= link_to "Log in", new_user_session_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> </div> + + <!-- Dark mode toggle button --> + <button id="theme-toggle" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white block" aria-label="Toggle dark mode"> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden dark:block" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /> + </svg> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 block dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor"> + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /> + </svg> + </button> <!-- Mobile hamburger menu for non-logged in users --> <div class="md:hidden relative ml-4"> @@ -131,6 +150,56 @@ </header> <script> + function initializeThemeToggle() { + const themeToggle = document.getElementById('theme-toggle'); + if (!themeToggle) return; + + // Remove any existing event listeners to prevent duplicates + const newThemeToggle = themeToggle.cloneNode(true); + themeToggle.parentNode.replaceChild(newThemeToggle, themeToggle); + + newThemeToggle.addEventListener('click', function() { + const htmlElement = document.documentElement; + const isDark = htmlElement.classList.contains('dark'); + const newMode = isDark ? 'light' : 'dark'; + + // Remove all theme classes + htmlElement.classList.remove('dark', 'light', 'auto'); + + // Add the new theme class + htmlElement.classList.add(newMode); + + // For logged-in users, save preference via AJAX + if (window.userSignedIn) { + fetch('/users/update_theme', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ darkmode: newMode }) + }).catch(error => console.error('Error updating theme preference:', error)); + } + // For logged-out users, save preference in localStorage + else { + localStorage.setItem('theme', newMode); + } + }); + + // If logged out and we have a theme in localStorage, apply it + if (!window.userSignedIn && localStorage.getItem('theme')) { + const savedTheme = localStorage.getItem('theme'); + if (savedTheme === 'dark' || savedTheme === 'light') { + const htmlElement = document.documentElement; + // Only apply if we're in 'auto' mode (default for logged-out users) + if (htmlElement.classList.contains('auto')) { + htmlElement.classList.remove('auto', 'dark', 'light'); + htmlElement.classList.add(savedTheme); + } + } + } + } + function initializeNavMenus() { // Initialize main menu for logged in users const menuToggle = document.getElementById('main-menu-toggle'); @@ -174,12 +243,24 @@ }); } + // Set user signed in state for JS + window.userSignedIn = <%= user_signed_in? %>; + // Initialize on DOMContentLoaded - document.addEventListener('DOMContentLoaded', initializeNavMenus); + document.addEventListener('DOMContentLoaded', () => { + initializeThemeToggle(); + initializeNavMenus(); + }); // Re-initialize on Turbo navigation - document.addEventListener('turbo:load', initializeNavMenus); - document.addEventListener('turbo:render', initializeNavMenus); + document.addEventListener('turbo:load', () => { + initializeThemeToggle(); + initializeNavMenus(); + }); + document.addEventListener('turbo:render', () => { + initializeThemeToggle(); + initializeNavMenus(); + }); </script> <main> diff --git a/config/routes.rb b/config/routes.rb index d1ba65249f9c286fa8ff512c475107cff166e75a..9879211ed8469dae7cbd46c73d2489b72bb7d8a5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,7 +24,9 @@ Rails.application.routes.draw do end get "speakers/show" + get "users/leaderboard" + patch "users/update_theme", to: "users#update_theme" # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live.