From afb3324c614bc05679dc9f7c44034f0d5ed6a060 Mon Sep 17 00:00:00 2001
From: Felix Eckhofer <felix@eckhofer.com>
Date: Wed, 1 Jan 2025 18:01:40 +0100
Subject: [PATCH] =?UTF-8?q?Add=20dark=20mode=20=F0=9F=8C=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 README.md                                     | 15 ++++++
 .../stylesheets/application.tailwind.css      | 19 ++++++-
 app/controllers/application_controller.rb     |  2 +-
 app/helpers/application_helper.rb             |  1 +
 app/javascript/application.js                 |  1 +
 app/models/user.rb                            |  3 ++
 app/views/assignments/by_user.html.erb        |  2 +-
 app/views/assignments/index.html.erb          |  2 +-
 app/views/assignments/show.html.erb           |  2 +-
 app/views/conferences/index.html.erb          |  4 +-
 app/views/conferences/show.html.erb           |  8 +--
 app/views/conferences/stats.html.erb          |  2 +-
 app/views/devise/registrations/edit.html.erb  |  7 ++-
 app/views/devise/registrations/new.html.erb   |  2 +-
 app/views/devise/sessions/new.html.erb        |  2 +-
 app/views/filedrop_files/download.html.erb    |  2 +-
 app/views/layouts/application.html.erb        | 53 ++++++++++++-------
 app/views/sessions/_session.html.erb          |  2 +-
 app/views/sessions/show.html.erb              | 18 +++----
 app/views/shared/_flash.html.erb              |  2 +-
 app/views/speakers/show.html.erb              |  2 +-
 app/views/users/leaderboard.html.erb          |  2 +-
 app/views/users/login.html.erb                |  2 +-
 app/views/users/profile.html.erb              |  2 +-
 config/tailwind.config.js                     |  1 +
 .../20250101154742_add_darkmode_to_users.rb   |  5 ++
 db/schema.rb                                  |  3 +-
 27 files changed, 113 insertions(+), 53 deletions(-)
 create mode 100644 db/migrate/20250101154742_add_darkmode_to_users.rb

diff --git a/README.md b/README.md
index 57b0ce5..8ba8a8c 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,21 @@ tbd.
 
 See [rescheduled-deploy](https://git.cccv.de/c3lingo/rescheduled-deploy) for a full docker-compose stack and more explanations.
 
+# Colors (darkmode)
+
+- **text**: text-slate-300
+- **text (overlay)**: text-slate-200
+- **text light**: text-slate-400
+
+- **logo**: text-white
+- **highlight**: text-red-500
+
+- **dark background**: bg-zinc-700
+- **light background**: bg-gray-900
+- **overlay**: bg-gray-600
+- **input**: bg-zinc-900
+- **shadow**: gray-400
+
 ## Tips and Tricks
 
 ### Helpful `rails` tasks
diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css
index beacefe..8e1ab0e 100644
--- a/app/assets/stylesheets/application.tailwind.css
+++ b/app/assets/stylesheets/application.tailwind.css
@@ -21,9 +21,24 @@ input[type=submit] {
     @apply bg-teal-800 text-teal-50 border-teal-600;
   }
 }
-select {
+
+select, [type=text], [type=password] {
   @apply pl-2 pr-6 py-1;
 }
+.dark {
+  input[type=submit] {
+    @apply bg-gray-600 text-slate-200;
+  }
+  select, [type=text], [type=password] {
+    @apply bg-zinc-900;
+  }
+  .session-holder {
+    select, [type=text], [type=password] {
+      @apply bg-white/60;
+    }
+  }
+}
+
 .session-holder {
   @apply w-full;
   height: var(--height);
@@ -85,4 +100,4 @@ select {
       @apply ml-6;
     }
   }
-}
\ No newline at end of file
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c964fde..4da35cb 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,7 +6,7 @@ class ApplicationController < ActionController::Base
   def configure_permitted_parameters
     devise_parameter_sanitizer.permit(:sign_up, keys: [:invitation_token])
     devise_parameter_sanitizer.permit(:account_update) do |u|
-      u.permit(:name, :email, :password, :password_confirmation, :avatar_color, :languages_from, :languages_to, :telegram_username, :current_password)
+      u.permit(:name, :email, :password, :password_confirmation, :avatar_color, :darkmode, :languages_from, :languages_to, :telegram_username, :current_password)
     end
   end
 
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d1a9e8a..268d351 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -4,6 +4,7 @@ module ApplicationHelper
     attributes[:loggedin_uid] = current_user.id if user_signed_in?
     attributes[:is_shiftcoordinator] = 1 if current_user&.shiftcoordinator?
     attributes[:languages_from] = current_user.languages_from unless current_user&.languages_from.blank?
+    attributes[:darkmode] = current_user.darkmode
     { data: attributes }
   end
 end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 0c66ece..9a7c38e 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -4,6 +4,7 @@ import "@hotwired/turbo-rails"
 
 document.addEventListener("turbo:load", function() {
   console.log('turbo:load');
+  applyDarkmode();
   const flashMessages = document.querySelectorAll(".flash");
 
   flashMessages.forEach(flashMessage => {
diff --git a/app/models/user.rb b/app/models/user.rb
index 1652be2..e54cc9c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -3,6 +3,9 @@ class User < ApplicationRecord
   has_many :assignments
   has_many :candidates
 
+  enum darkmode: { auto: 0, light: 1, dark: 2 }
+  validates :darkmode, inclusion: { in: %w(auto light dark) }
+
   validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
   validates :name, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: false
   validates :email, uniqueness: { case_sensitive: false, message: "already in use" }, allow_nil: true, allow_blank: true
diff --git a/app/views/assignments/by_user.html.erb b/app/views/assignments/by_user.html.erb
index bb5542c..9f24670 100644
--- a/app/views/assignments/by_user.html.erb
+++ b/app/views/assignments/by_user.html.erb
@@ -1,5 +1,5 @@
 <div>
-  <h1 class="text-xl my-4">
+  <h1 class="text-xl my-4 dark:text-red-500">
     Assignments for
     <%= link_to @user.name, user_assignments_path(@user) %>
     <span class="text-base ml-2 mb-2 inline p-2 border bg-slate-50 hover:bg-slate-100 border-slate-200 hover:border-slate-200 shadow font-normal rounded-md"><%= link_to user_assignments_path(@user, format: 'ics') do %><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-6 inline-block stroke-slate-400 fill-slate-400"><path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd"></path></svg> iCal<% end %></span>
diff --git a/app/views/assignments/index.html.erb b/app/views/assignments/index.html.erb
index 470a815..8b61ac6 100644
--- a/app/views/assignments/index.html.erb
+++ b/app/views/assignments/index.html.erb
@@ -1,6 +1,6 @@
 <% now = Time.now %>
 <div class="scroll-smooth">
-  <h1 class="text-xl my-4">Assignments for all users</h1>
+  <h1 class="text-xl my-4 dark:text-red-500">Assignments for all users</h1>
   <p>
     Jump to:
     <ul class="flex flex-row flex-wrap">
diff --git a/app/views/assignments/show.html.erb b/app/views/assignments/show.html.erb
index 6c53075..be9f69e 100644
--- a/app/views/assignments/show.html.erb
+++ b/app/views/assignments/show.html.erb
@@ -1,4 +1,4 @@
 <div>
-  <h1 class="font-bold text-4xl">Assignments#show</h1>
+  <h1 class="font-bold text-4xl dark:text-red-500">Assignments#show</h1>
   <p>Find me in app/views/assignments/show.html.erb</p>
 </div>
diff --git a/app/views/conferences/index.html.erb b/app/views/conferences/index.html.erb
index 7de75c2..4fcb2ed 100644
--- a/app/views/conferences/index.html.erb
+++ b/app/views/conferences/index.html.erb
@@ -1,8 +1,8 @@
 <div>
-<h1 class="text-xl my-4">Conferences</h1>
+<h1 class="text-xl my-4 dark:text-red-500">Conferences</h1>
 <ul>
 <% @conferences.each do |conference| %>
-<li><%= link_to conference.name, conference_path(conference.slug), class: "inline-block px-4 py-2 text-slate-500 hover:text-slate-900 hover:bg-slate-100 rounded-md" %></li>
+<li><%= link_to conference.name, conference_path(conference.slug), class: "inline-block px-4 py-2 text-slate-500 hover:text-slate-900 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-gray-600 rounded-md" %></li>
 <% end %>
 </ul>
 </div>
diff --git a/app/views/conferences/show.html.erb b/app/views/conferences/show.html.erb
index 8c17f0e..0528867 100644
--- a/app/views/conferences/show.html.erb
+++ b/app/views/conferences/show.html.erb
@@ -17,7 +17,7 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone)
   <div>
     <a href="#now" onclick="document.querySelector('#now')?.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); return false" class="underline text-blue-500">Jump to current time</a>
   </div>
-  <h1 class="text-2xl font-bold my-2"><%= @conference.name %></h1>
+  <h1 class="text-2xl font-bold my-2 dark:text-red-500"><%= @conference.name %></h1>
   <p class="text-xs mb-6">
     <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="inline-block size-4 stroke-slate-500">
       <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
@@ -58,7 +58,7 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone)
     timeline_ends_at = day_ends_at.advance(minutes: (day_ends_at.min / timeline_granularity.to_f).ceil * timeline_granularity)
   %>
     <div class="conference-day my-8" id="<%= date.strftime('day-%Y-%m-%d') %>">
-      <h3 class="text-xl my-4 border-b sticky top-0 md:top-[3.7rem] bg-white bg-opacity-70 z-30"><%= date.strftime('%B %d, %Y') %></h3>
+      <h3 class="text-xl my-4 border-b sticky top-0 md:top-[3.7rem] bg-white dark:bg-gray-900 bg-opacity-70 dark:bg-opacity-70 z-30"><%= date.strftime('%B %d, %Y') %></h3>
       <%
 =begin%>
       <ul class="list-disc">
@@ -71,7 +71,7 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone)
 =end %>
       <div class="day-wrapper flex relative">
 
-      <div class="times sticky left-0 bg-white bg-opacity-70 z-20">
+      <div class="times sticky left-0 bg-white dark:bg-gray-900 bg-opacity-70 dark:bg-opacity-70 z-20">
         <h4>Time</h4>
         <% # if current_time.strftime('%Y%m%d') == date.strftime('%Y%m%d') && current_time >= timeline_starts_at && current_time <= timeline_ends_at
         %>
@@ -131,7 +131,7 @@ current_time = Time.zone.now.in_time_zone(@conference.time_zone)
         sessions = @sessions_by_date_and_stage[date][stage]
       %>
           <div class="stage">
-            <h4 class="sticky md:top-[5.5rem] top-7 bg-white bg-opacity-70 w-full z-30"><%= stage.name %></h4>
+            <h4 class="sticky md:top-[5.5rem] top-7 bg-white dark:bg-gray-900 bg-opacity-70 dark:bg-opacity-70 w-full z-30"><%= stage.name %></h4>
             <div class="stage-sessions">
               <% sessions.each do |session| %>
                 <div class="session-holder hover:z-30 h-full" style="
diff --git a/app/views/conferences/stats.html.erb b/app/views/conferences/stats.html.erb
index 8465f00..5ae8cfb 100644
--- a/app/views/conferences/stats.html.erb
+++ b/app/views/conferences/stats.html.erb
@@ -1,5 +1,5 @@
 <div>
-  <h1>Statistics for <%= @conference.name %></h1>
+  <h1 class="dark:text-red-500">Statistics for <%= @conference.name %></h1>
   <div class="conference-stats">
     <ul>
       <li><%= @assignees.count %> interpreters
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb
index b517dde..5fa03b7 100644
--- a/app/views/devise/registrations/edit.html.erb
+++ b/app/views/devise/registrations/edit.html.erb
@@ -1,6 +1,6 @@
 <div>
 
-<h1 class="text-xl my-4">Profile</h1>
+<h1 class="text-xl my-4 dark:text-red-500">Profile</h1>
 
 <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
   <%= render "devise/shared/error_messages", resource: resource %>
@@ -33,6 +33,11 @@
     <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
   </div>
 
+  <div class="field">
+    <%= f.label :darkmode %>
+    <%= f.select :darkmode, User.darkmodes.keys.map { |d| [d.humanize, d] } %>
+  </div>
+
   <div class="field">
     <%= f.label :avatar_color %>
     <%= f.color_field :avatar_color %>
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb
index 5d98cd4..b532322 100644
--- a/app/views/devise/registrations/new.html.erb
+++ b/app/views/devise/registrations/new.html.erb
@@ -1,5 +1,5 @@
 <div>
-<h1 class="text-xl my-4">Sign Up</h1>
+<h1 class="text-xl my-4 dark:text-red-500">Sign Up</h1>
 
 <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
   <%= render "devise/shared/error_messages", resource: resource %>
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
index a216b2d..70ca053 100644
--- a/app/views/devise/sessions/new.html.erb
+++ b/app/views/devise/sessions/new.html.erb
@@ -1,5 +1,5 @@
 <div>
-<h1 class="text-xl my-4">Log in</h1>
+<h1 class="text-xl my-4 dark:text-red-500">Log in</h1>
 
 <%= form_for(resource, as: resource_name, url: user_session_path) do |f| %>
   <div class="field">
diff --git a/app/views/filedrop_files/download.html.erb b/app/views/filedrop_files/download.html.erb
index 60788e4..a1ee394 100644
--- a/app/views/filedrop_files/download.html.erb
+++ b/app/views/filedrop_files/download.html.erb
@@ -1,4 +1,4 @@
 <div>
-  <h1 class="font-bold text-4xl">FiledropFiles#download</h1>
+  <h1 class="font-bold text-4xl dark:text-red-500">FiledropFiles#download</h1>
   <p>Find me in app/views/filedrop_files/download.html.erb</p>
 </div>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 7a482d3..e2c68a9 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -9,52 +9,65 @@
 
     <%= stylesheet_link_tag "application" %>
     <%= javascript_importmap_tags %>
+
+    <script type="text/javascript">
+      function applyDarkmode() {
+        const userTheme = document.body.dataset.darkmode;
+        document.documentElement.classList.toggle(
+          'dark',
+          userTheme === 'dark' || ((userTheme === 'auto') && window.matchMedia('(prefers-color-scheme: dark)').matches)
+        );
+      }
+    </script>
   </head>
 
-  <body <%= tag.attributes(body_data_attributes) %>>
-  <nav class="bg-slate-100 text-gray-700 shadow w-full relative md:fixed z-50" data-controller="navigation">
-    <div class="relative bg-slate-100 z-50 container mx-auto px-4 py-4 flex justify-between items-center">
+  <body <%= tag.attributes(body_data_attributes) %> class="dark:bg-gray-900 dark:text-slate-300">
+  <script type="text/javascript">
+    applyDarkmode(); // Required to prevent flashing on load
+  </script>
+  <nav class="bg-slate-100 dark:bg-zinc-700 text-gray-700 dark:text-slate-300 shadow w-full relative md:fixed z-50" data-controller="navigation">
+    <div class="relative bg-slate-100 dark:bg-zinc-700 z-50 container mx-auto px-4 py-4 flex justify-between items-center">
       <div class="flex items-center space-x-4">
-        <div class="text-xl font-bold text-black"><%= link_to 're:scheduled', '/', class: "!no-underline" %></div>
+        <div class="text-xl font-bold text-black dark:text-white"><%= link_to 're:scheduled', '/', class: "!no-underline" %></div>
         <div class="hidden md:flex space-x-4">
-          <%= link_to 'Conferences', conferences_path, class: 'hover:text-gray-900' %>
-          <%= link_to 'Assignments', assignments_path, class: 'hover:text-gray-900' %>
+          <%= link_to 'Conferences', conferences_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
+          <%= link_to 'Assignments', assignments_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
         </div>
       </div>
       <div class="hidden md:flex items-center space-x-4 ml-0 lg:ml-8">
         <% if user_signed_in? %>
         <span class="-mr-2 hidden lg:inline">logged in as</span>
         <%= render partial: 'application/user_avatar', locals: { user: current_user } %>
-        <%= link_to '<span class="hidden lg:inline">My </span>Profile'.html_safe, edit_user_registration_path, class: 'hover:text-gray-900', aria_label: "My Profile" %>
-        <%= link_to 'My Assignments', user_assignments_path(current_user), class: 'hover:text-gray-900' %>
-        <%= link_to 'Logout', destroy_user_session_path, data: { turbo_method: :delete }, class: 'hover:text-gray-900' %>
+        <%= link_to '<span class="hidden lg:inline">My </span>Profile'.html_safe, edit_user_registration_path, class: 'hover:text-gray-900 dark:hover:text-slate-200', aria_label: "My Profile" %>
+        <%= link_to 'My Assignments', user_assignments_path(current_user), class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
+        <%= link_to 'Logout', destroy_user_session_path, data: { turbo_method: :delete }, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
         <% else %>
         <span class="px-2">not logged in</span>
-        <%= link_to 'Login', new_user_session_path, class: 'hover:text-gray-900' %>
-        <%= link_to 'Sign Up', new_user_registration_path, class: 'hover:text-gray-900' %>
+        <%= link_to 'Login', new_user_session_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
+        <%= link_to 'Sign Up', new_user_registration_path, class: 'hover:text-gray-900 dark:hover:text-slate-200' %>
         <% end %>
       </div>
       <div class="md:hidden">
-        <button id="menu-button" class="text-gray-700 hover:text-gray-900 focus:outline-none" data-action="click->navigation#toggleMenu">
+        <button id="menu-button" class="text-gray-700 dark:text-slate-400 hover:text-gray-900 dark:hover:text-white focus:outline-none" data-action="click->navigation#toggleMenu">
           <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
             <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
           </svg>
         </button>
       </div>
     </div>
-    <div id="mobile-menu" class="absolute top-18 bg-slate-100 z-20 container mx-auto hidden md:hidden space-y-4 px-8 pb-4 shadow" data-navigation-target="mobileMenu">
-        <%= link_to 'Conferences', conferences_path, class: 'block hover:text-gray-900' %>
-        <%= link_to 'Assignments', assignments_path, class: 'block hover:text-gray-900' %>
+    <div id="mobile-menu" class="absolute top-18 bg-slate-100 dark:bg-zinc-700 z-20 container mx-auto hidden md:hidden space-y-4 px-8 pb-4 shadow" data-navigation-target="mobileMenu">
+        <%= link_to 'Conferences', conferences_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
+        <%= link_to 'Assignments', assignments_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
         <hr>
         <% if user_signed_in? %>
         <div>logged in as <%= render partial: 'application/user_avatar', locals: { user: current_user } %></div>
-        <%= link_to 'My Profile', edit_user_registration_path, class: 'block hover:text-gray-900' %>
-        <%= link_to 'My Assignments', user_assignments_path(current_user), class: 'block hover:text-gray-900' %>
-        <%= link_to 'Logout', destroy_user_session_path, data: { turbo_method: :delete }, class: 'block hover:text-gray-900' %>
+        <%= link_to 'My Profile', edit_user_registration_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
+        <%= link_to 'My Assignments', user_assignments_path(current_user), class: 'block hover:text-gray-900 dark:hover:text-white' %>
+        <%= link_to 'Logout', destroy_user_session_path, data: { turbo_method: :delete }, class: 'block hover:text-gray-900 dark:hover:text-white' %>
         <% else %>
         <div>not logged in</div>
-        <%= link_to 'Login', new_user_session_path, class: 'block hover:text-gray-900' %>
-        <%= link_to 'Sign Up', new_user_registration_path, class: 'block hover:text-gray-900' %>
+        <%= link_to 'Login', new_user_session_path, class: 'block hover:text-gray-900 dark:hover:text-white' %>
+        <%= link_to 'Sign Up', new_user_registration_path, class: 'block hover:text-gray-900 dark:hover:text-white  ' %>
         <% end %>
     </div>
   </nav>
diff --git a/app/views/sessions/_session.html.erb b/app/views/sessions/_session.html.erb
index 623dd04..b0584be 100644
--- a/app/views/sessions/_session.html.erb
+++ b/app/views/sessions/_session.html.erb
@@ -1,6 +1,6 @@
 <% unassigned_users = User.all - session.assignments.collect(&:user) %>
 <%= turbo_frame_tag dom_id(session), method: "morph", class: "w-full", data: { controller: "session", language: session.language } do %>
-  <div class="session shadow hover:shadow-lg overflow-scroll text-sm w-full !h-full min-h-full hover:!min-h-max <%= session.translators_needed? ? "translators-needed" : "no-translators-needed" %> <%= session.backup_needed? ? "backup-needed" : "no-backup-needed" %> <%= session.assignees? ? "has-assignees" : "no-assignees" %> <%= (session.ends_at < Time.now ? "past" : "") %>">
+  <div class="session dark:text-black shadow hover:shadow-lg overflow-scroll text-sm w-full !h-full min-h-full hover:!min-h-max <%= session.translators_needed? ? "translators-needed" : "no-translators-needed" %> <%= session.backup_needed? ? "backup-needed" : "no-backup-needed" %> <%= session.assignees? ? "has-assignees" : "no-assignees" %> <%= (session.ends_at < Time.now ? "past" : "") %>">
     <h4>
       <small class="text-2xs uppercase font-bold <%= session.language=="de" ? "text-sky-700 border-sky-700" : "text-fuchsia-700 border-fuchsia-700" %> border bg-black/10 rounded-sm p-1 mr-1 lang-<%= session.language %>"><%= session.language %></small><% unless session.recorded %><span aria-label="Session is not recorded" title="Session is not recorded"><svg class="inline-block -mt-0.5 w-5 h-5" fill="#000000" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m255-1c-141 0-256 115-256 256s115 256 256 256 256-115 256-256-115-256-256-256zm0 17c63 0 121 25 163 65l-65 65-19-48c-0.85-2.6-4.3-5.1-7.7-5.1h-142c-3.4 0-6 1.7-7.7 5.1l-21 55h-49c-17 0-31 14-31 31v169c0 17 14 31 31 31h6.7l-34 34c-39-43-64-100-64-162 0-131 108-239 239-239zm60 239c0 33-26 60-60 60-14 0-26-4.4-36-12l84-83c7.3 9.9 12 22 12 36zm-119 0c0-33 26-60 60-60 14 0 26 4.5 36 12l-84 83c-7.4-9.9-12-22-12-36zm108-60c-13-11-30-17-48-17-43 0-77 34-77 77 0 18 6.2 35 17 48l-64 63h-25c-7.7 0-14-6-14-14v-169c0-7.7 6-14 14-14h55c3.4 0 6.8-2.6 7.7-5.1l21-55h130l19 50-36 36zm-97 120c13 11 30 17 48 17 43 0 77-34 77-77 0-18-6.1-35-16-48l38-38h49c7.7 0 14 6 14 14v169c0 7.7-6 14-14 14h-247l51-51zm48 179c-63 0-120-25-163-65l46-46h265c17 0 31-14 32-31v-169c0-17-14-31-31-31h-34l60-59c39 43 64 100 64 162-1e-3 131-108 239-239 239z"/><path d="m383 187c-9.4 0-17 7.7-17 17s7.7 17 17 17 17-7.7 17-17-7.7-17-17-17z"/></svg></span><% end %><%= render partial: 'shared/session_filedrop', locals: { session: } %>
       <%= link_to session.title, session.url, target: "_blank" %>
diff --git a/app/views/sessions/show.html.erb b/app/views/sessions/show.html.erb
index 39a2b53..3ce9d3d 100644
--- a/app/views/sessions/show.html.erb
+++ b/app/views/sessions/show.html.erb
@@ -1,6 +1,6 @@
 <div>
   <h6><%= link_to @session.conference.name, @session.conference %></h6>
-  <h1>
+  <h1 class="dark:text-red-500">
     <%= @session.title %>
     <% unless @session.url.blank? %>
     <%= link_to "🔗 open in Fahrplan", @session.url, class: "ml-4 font-normal" %>
@@ -15,9 +15,9 @@
   <h3 class="mt-4">Comments <span class="font-normal">from Speakers' Filedrop</span></h2>
   <ul class="space-y-4 my-4">
     <% @session.filedrop_comments.each do |comment| %>
-    <li class="bg-gray-100 shadow rounded-lg p-2">
-      <div class="text-gray-900 whitespace-pre-wrap"><%= comment.body %></div>
-      <div class="text-gray-500 text-sm mt-2">
+    <li class="text-gray-900 dark:text-slate-200 bg-gray-100 dark:bg-gray-600 shadow rounded-lg p-2">
+      <div class="whitespace-pre-wrap"><%= comment.body %></div>
+      <div class="text-sm mt-2">
         <%= comment.orig_created&.in_time_zone(@session.conference.time_zone || 'UTC')&.strftime("%B %d, %Y %H:%M") %>
       </div>
     </li>
@@ -29,13 +29,13 @@
   <h3 class="mt-4">Files <span class="font-normal">from Speakers' Filedrop</span></h3>
   <ul class="space-y-4 my-4">
     <% @session.filedrop_files.each do |file| %>
-    <li class="bg-white shadow rounded-lg p-4">
+    <li class="text-gray-500 dark:text-slate-400 shadow dark:shadow-gray-500 rounded-lg p-4">
         <div class="flex flex-wrap justify-between items-start max-w-full">
           <div>
-            <div class="text-gray-900 text-lg font-semibold"><%= file.name %></div>
-            <div class="text-gray-500 text-sm">Size: <%= number_to_human_size(file.size) %></div>
-            <div class="text-gray-500 text-sm">Date: <%=file.orig_created&.in_time_zone(@session.conference.time_zone || 'UTC')&.strftime("%B %d, %Y %H:%M") %></div>
-            <div class="text-gray-500 text-sm">Checksum: <%= file.checksum %></div>
+            <div class="text-gray-900 dark:text-slate-300 text-lg font-semibold"><%= file.name %></div>
+            <div class="text-sm">Size: <%= number_to_human_size(file.size) %></div>
+            <div class="text-sm">Date: <%=file.orig_created&.in_time_zone(@session.conference.time_zone || 'UTC')&.strftime("%B %d, %Y %H:%M") %></div>
+            <div class="text-sm">Checksum: <%= file.checksum %></div>
           </div>
         <div class="mt-2 sm:mt-0 flex-shrink-0">
           <%= link_to 'Download', download_filedrop_file_path(file), class: "bg-blue-500 text-white px-3 py-2 rounded" %>
diff --git a/app/views/shared/_flash.html.erb b/app/views/shared/_flash.html.erb
index aff4cb5..ef5b20c 100644
--- a/app/views/shared/_flash.html.erb
+++ b/app/views/shared/_flash.html.erb
@@ -1,4 +1,4 @@
-<div id="flash" class="relative top-0.5 md:top-16">
+<div id="flash" class="relative top-0.5 md:top-16 dark:text-black">
   <% flash.each do |type, message| %>
     <div class="flash alert alert-<%= type %>">
       <%= message %>
diff --git a/app/views/speakers/show.html.erb b/app/views/speakers/show.html.erb
index 5780d9e..aa4a484 100644
--- a/app/views/speakers/show.html.erb
+++ b/app/views/speakers/show.html.erb
@@ -1,5 +1,5 @@
 <div>
-  <h1 class="font-bold text-4xl"><%= @speaker.name %></h1>
+  <h1 class="font-bold text-4xl dark:text-red-500"><%= @speaker.name %></h1>
   <h2 class="font-medium text-2xl"><%= @speaker.position %></h2>
   <p><%= @speaker.description.html_safe %></p>
   <% # = link_to "#{@speaker.name} at #{@speaker.conference.name}", %>
diff --git a/app/views/users/leaderboard.html.erb b/app/views/users/leaderboard.html.erb
index fb22a0c..0bf5ae0 100644
--- a/app/views/users/leaderboard.html.erb
+++ b/app/views/users/leaderboard.html.erb
@@ -1,5 +1,5 @@
 <div class="w-full">
-  <h1 class="text-xl my-4">Leaderboard</h1>
+  <h1 class="text-xl my-4 dark:text-red-500">Leaderboard</h1>
     <dl class="w-full max-w-4xl mx-auto">
       <% top_workload = @workload_data.values.max %>
       <% @workload_data.each_with_index do |(username, workload), index| %>
diff --git a/app/views/users/login.html.erb b/app/views/users/login.html.erb
index 7c99ca8..6b50d7e 100644
--- a/app/views/users/login.html.erb
+++ b/app/views/users/login.html.erb
@@ -1,5 +1,5 @@
 <div>
-  <h1 class="font-bold text-4xl mb-8">Choose User</h1>
+  <h1 class="font-bold text-4xl mb-8 dark:text-red-500">Choose User</h1>
   <ul class="flex flex-wrap gap-2">
   <% @users.each do |user| %>
     <li>
diff --git a/app/views/users/profile.html.erb b/app/views/users/profile.html.erb
index b79fdb8..b58bff8 100644
--- a/app/views/users/profile.html.erb
+++ b/app/views/users/profile.html.erb
@@ -1,5 +1,5 @@
 <div>
-  <h1 class="font-bold text-4xl">Profile</h1>
+  <h1 class="font-bold text-4xl dark:text-red-500">Profile</h1>
   <%= form_with(model: @user, url: update_profile_path, local: true) do |form| %>
 
     <div class="field">
diff --git a/config/tailwind.config.js b/config/tailwind.config.js
index 06dd449..71a0f8b 100644
--- a/config/tailwind.config.js
+++ b/config/tailwind.config.js
@@ -7,6 +7,7 @@ module.exports = {
     './app/javascript/**/*.js',
     './app/views/**/*.{erb,haml,html,slim}'
   ],
+  darkMode: 'selector',
   theme: {
     extend: {
       keyframes: {
diff --git a/db/migrate/20250101154742_add_darkmode_to_users.rb b/db/migrate/20250101154742_add_darkmode_to_users.rb
new file mode 100644
index 0000000..72aaf7e
--- /dev/null
+++ b/db/migrate/20250101154742_add_darkmode_to_users.rb
@@ -0,0 +1,5 @@
+class AddDarkmodeToUsers < ActiveRecord::Migration[7.1]
+  def change
+    add_column :users, :darkmode, :integer, null: false, default: 0
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 707b098..d8b064f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema[7.1].define(version: 2024_12_30_120900) do
+ActiveRecord::Schema[7.1].define(version: 2025_01_01_154742) do
   create_table "assignments", force: :cascade do |t|
     t.integer "user_id", null: false
     t.integer "session_id", null: false
@@ -307,6 +307,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_12_30_120900) do
     t.string "invitation_token"
     t.string "languages_from"
     t.string "languages_to"
+    t.integer "darkmode", default: 0, null: false
   end
 
   add_foreign_key "assignments", "sessions"
-- 
GitLab