From d82c5e9e859211bf0b53de4f07fee1cdb6860739 Mon Sep 17 00:00:00 2001 From: Teal Bauer <teal@starsong.eu> Date: Sun, 9 Mar 2025 23:21:24 +0100 Subject: [PATCH] Extend dark mode, return sticky headers for assignments --- app/views/assignments/_listview_date.html.erb | 2 +- app/views/assignments/by_user.html.erb | 175 ++++++++++++------ app/views/devise/confirmations/new.html.erb | 16 ++ .../mailer/confirmation_instructions.html.erb | 5 + .../devise/mailer/email_changed.html.erb | 7 + .../devise/mailer/password_change.html.erb | 3 + .../reset_password_instructions.html.erb | 8 + .../mailer/unlock_instructions.html.erb | 7 + app/views/devise/passwords/edit.html.erb | 29 +++ app/views/devise/passwords/new.html.erb | 20 ++ app/views/devise/registrations/edit.html.erb | 117 ++++++++---- .../devise/shared/_error_messages.html.erb | 15 ++ app/views/devise/unlocks/new.html.erb | 16 ++ app/views/layouts/application.html.erb | 38 +++- 14 files changed, 364 insertions(+), 94 deletions(-) create mode 100644 app/views/devise/confirmations/new.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/email_changed.html.erb create mode 100644 app/views/devise/mailer/password_change.html.erb create mode 100644 app/views/devise/mailer/reset_password_instructions.html.erb create mode 100644 app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 app/views/devise/passwords/edit.html.erb create mode 100644 app/views/devise/passwords/new.html.erb create mode 100644 app/views/devise/shared/_error_messages.html.erb create mode 100644 app/views/devise/unlocks/new.html.erb diff --git a/app/views/assignments/_listview_date.html.erb b/app/views/assignments/_listview_date.html.erb index 307753e..058a632 100644 --- a/app/views/assignments/_listview_date.html.erb +++ b/app/views/assignments/_listview_date.html.erb @@ -1,5 +1,5 @@ <div class="mb-6 <%= Time.parse(date).end_of_day < now ? "past" : "future" %>"> - <h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3"><%= date %></h3> + <h3 class="sticky top-0 z-10 text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3 bg-white dark:bg-gray-800 py-2"><%= date %></h3> <ul class="space-y-3"> <% assignments_on_date.each do |assignment| %> <li class="<%= assignment.session.starts_at < now ? "past" : "future" %> pl-4 border-l-4 border-gray-200 dark:border-gray-700"> diff --git a/app/views/assignments/by_user.html.erb b/app/views/assignments/by_user.html.erb index a3a8615..e030d9c 100644 --- a/app/views/assignments/by_user.html.erb +++ b/app/views/assignments/by_user.html.erb @@ -1,60 +1,129 @@ <div class="container mx-auto px-4 py-8"> <div class="max-w-full"> - <div class="flex items-center mb-6"> - <h1 class="text-2xl font-bold dark:text-gray-200"> - Assignments for - <%= link_to @user.name, user_assignments_path(@user), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> - </h1> - <%= link_to user_assignments_path(@user, format: 'ics'), class: "btn btn-info-light ml-4" do %> - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><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 %> - </div> - <% now = Time.now %> - <div class="mb-8"> - <h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">List View</h2> - <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6"> - <% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').group_by { |a| a.session.starts_at.strftime('%Y-%m-%d') }.each do |date, assignments_on_date| %> - <%= render partial: 'listview_date', locals: { assignments_on_date:, date:, now: } %> + <div class="flex items-center mb-6"> + <h1 class="text-2xl font-bold dark:text-gray-200"> + Assignments for + <%= link_to @user.name, user_assignments_path(@user), class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> + </h1> + <%= link_to user_assignments_path(@user, format: 'ics'), class: "btn btn-info-light ml-4" do %> + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 24" fill="currentColor" aria-hidden="true" class="size-5 inline-block mr-1"><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 %> </div> - </div> - - <div class="overflow-x-auto max-w-full"> - <h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4 sticky left-0">Table View</h2> - <div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg"> - <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> - <thead class="bg-gray-50 dark:bg-gray-700"> - <tr> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Date</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Starts</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Ends</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Stage</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Session</th> - <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Collaborators</th> - </tr> - </thead> - <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> - <% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').each do |assignment| %> - <tr class="<%= assignment.session.ends_at < Time.now ? "past" : "future" %>"> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%Y-%m-%d') %></td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%H:%M') %></td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.ends_at.strftime('%H:%M') %></td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.stage.name %></td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-200"> - <%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %> - <%= link_to assignment.session.title, assignment.session.url, target: "_blank", class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> - <%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %> - </td> - <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> - <% assignment.session.assignments.map(&:user).each do |other_user| %> - <%= render partial: 'application/user_avatar', locals: { user: other_user } %> - <% end %> - </td> - </tr> - <% end %> - </tbody> - </table> + <% now = Time.now %> + + <!-- Tab Navigation --> + <div class="border-b border-gray-200 dark:border-gray-700 mb-6"> + <ul class="flex flex-wrap -mb-px" id="viewTabs" role="tablist"> + <li class="mr-2" role="presentation"> + <button class="inline-block py-3 px-4 text-blue-600 dark:text-blue-500 font-medium rounded-t-lg border-b-2 border-blue-600 dark:border-blue-500 active" + id="list-tab" + data-tabs-target="#listViewTab" + type="button" + role="tab" + aria-controls="listViewTab" + aria-selected="true"> + List View + </button> + </li> + <li class="mr-2" role="presentation"> + <button class="inline-block py-3 px-4 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 font-medium rounded-t-lg border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-600" + id="table-tab" + data-tabs-target="#tableViewTab" + type="button" + role="tab" + aria-controls="tableViewTab" + aria-selected="false"> + Table View + </button> + </li> + </ul> + </div> + + <!-- Tab Content --> + <div id="tabContent" style="min-height: calc(100vh - 220px);"> + <!-- List View Tab --> + <div class="block" id="listViewTab" role="tabpanel" aria-labelledby="list-tab"> + <div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6 overflow-auto" style="max-height: calc(100vh - 220px);"> + <% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').group_by { |a| a.session.starts_at.strftime('%Y-%m-%d') }.each do |date, assignments_on_date| %> + <%= render partial: 'listview_date', locals: { assignments_on_date:, date:, now: } %> + <% end %> + </div> + </div> + + <!-- Table View Tab --> + <div class="hidden" id="tableViewTab" role="tabpanel" aria-labelledby="table-tab"> + <div class="bg-white dark:bg-gray-800 shadow overflow-hidden rounded-lg"> + <div class="overflow-auto" style="max-height: calc(100vh - 220px);"> + <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> + <thead> + <tr> + <th scope="col" class="sticky top-0 bg-gray-50 dark:bg-gray-700 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider z-10">Date</th> + <th scope="col" class="sticky top-0 bg-gray-50 dark:bg-gray-700 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider z-10">Starts</th> + <th scope="col" class="sticky top-0 bg-gray-50 dark:bg-gray-700 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider z-10">Ends</th> + <th scope="col" class="sticky top-0 bg-gray-50 dark:bg-gray-700 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider z-10">Stage</th> + <th scope="col" class="sticky top-0 bg-gray-50 dark:bg-gray-700 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider z-10">Session</th> + <th scope="col" class="sticky top-0 bg-gray-50 dark:bg-gray-700 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider z-10">Collaborators</th> + </tr> + </thead> + <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> + <% @user.assignments.includes(:session, session: :conference).order('sessions.starts_at').each do |assignment| %> + <tr class="<%= assignment.session.ends_at < Time.now ? "past" : "future" %>"> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%Y-%m-%d') %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.starts_at.strftime('%H:%M') %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.ends_at.strftime('%H:%M') %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= assignment.session.stage.name %></td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-200"> + <%= render partial: 'shared/session_filedrop', locals: { session: assignment.session } %> + <%= link_to assignment.session.title, assignment.session.url, target: "_blank", class: "text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" %> + <%= render partial: 'shared/session_engelsystem', locals: { session: assignment.session } %> + </td> + <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> + <% assignment.session.assignments.map(&:user).each do |other_user| %> + <%= render partial: 'application/user_avatar', locals: { user: other_user } %> + <% end %> + </td> + </tr> + <% end %> + </tbody> + </table> + </div> + </div> + </div> </div> </div> </div> -</div> + +<script> + document.addEventListener('DOMContentLoaded', function() { + const tabButtons = document.querySelectorAll('[data-tabs-target]'); + const tabContents = document.querySelectorAll('[role="tabpanel"]'); + + tabButtons.forEach(button => { + button.addEventListener('click', () => { + const target = document.querySelector(button.dataset.tabsTarget); + + // Hide all tabs + tabContents.forEach(content => { + content.classList.add('hidden'); + content.classList.remove('block'); + }); + + // Remove active state from all buttons + tabButtons.forEach(btn => { + btn.classList.remove('text-blue-600', 'dark:text-blue-500', 'border-blue-600', 'dark:border-blue-500', 'active'); + btn.classList.add('text-gray-500', 'dark:text-gray-400', 'border-transparent'); + btn.setAttribute('aria-selected', 'false'); + }); + + // Show the selected tab + target.classList.remove('hidden'); + target.classList.add('block'); + + // Set active state on clicked button + button.classList.remove('text-gray-500', 'dark:text-gray-400', 'border-transparent'); + button.classList.add('text-blue-600', 'dark:text-blue-500', 'border-blue-600', 'dark:border-blue-500', 'active'); + button.setAttribute('aria-selected', 'true'); + }); + }); + }); +</script> diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000..b12dd0c --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,16 @@ +<h2>Resend confirmation instructions</h2> + +<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + + <div class="field"> + <%= f.label :email %><br /> + <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> + </div> + + <div class="actions"> + <%= f.submit "Resend confirmation instructions" %> + </div> +<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..dc55f64 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +<p>Welcome <%= @email %>!</p> + +<p>You can confirm your account email through the link below:</p> + +<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p> diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb new file mode 100644 index 0000000..32f4ba8 --- /dev/null +++ b/app/views/devise/mailer/email_changed.html.erb @@ -0,0 +1,7 @@ +<p>Hello <%= @email %>!</p> + +<% if @resource.try(:unconfirmed_email?) %> + <p>We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.</p> +<% else %> + <p>We're contacting you to notify you that your email has been changed to <%= @resource.email %>.</p> +<% end %> diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb new file mode 100644 index 0000000..b41daf4 --- /dev/null +++ b/app/views/devise/mailer/password_change.html.erb @@ -0,0 +1,3 @@ +<p>Hello <%= @resource.email %>!</p> + +<p>We're contacting you to notify you that your password has been changed.</p> diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..f667dc1 --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +<p>Hello <%= @resource.email %>!</p> + +<p>Someone has requested a link to change your password. You can do this through the link below.</p> + +<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p> + +<p>If you didn't request this, please ignore this email.</p> +<p>Your password won't change until you access the link above and create a new one.</p> diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..41e148b --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +<p>Hello <%= @resource.email %>!</p> + +<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p> + +<p>Click the link below to unlock your account:</p> + +<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p> diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000..3d7c2e7 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,29 @@ +<div class="container mx-auto px-4 py-8"> + <div class="max-w-md mx-auto bg-white dark:bg-gray-800 shadow rounded-lg p-6"> + <h1 class="text-2xl font-bold mb-6 dark:text-white">Change your password</h1> + + <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + <%= f.hidden_field :reset_password_token %> + + <div class="mb-4"> + <%= f.label :password, "New password", class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <% if @minimum_password_length %> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">(<%= @minimum_password_length %> characters minimum)</p> + <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-4"> + <%= f.label :password_confirmation, "Confirm new password", class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-6"> + <%= f.submit "Change my password", class: "w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" %> + </div> + <% end %> + + <%= render "devise/shared/links" %> + </div> +</div> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000..da798dd --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,20 @@ +<div class="container mx-auto px-4 py-8"> + <div class="max-w-md mx-auto bg-white dark:bg-gray-800 shadow rounded-lg p-6"> + <h1 class="text-2xl font-bold mb-6 dark:text-white">Forgot your password?</h1> + + <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + + <div class="mb-4"> + <%= f.label :email, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-6"> + <%= f.submit "Send me reset password instructions", class: "w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:bg-blue-700 dark:hover:bg-blue-800" %> + </div> + <% end %> + + <%= render "devise/shared/links" %> + </div> +</div> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 3c5be50..39a1104 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -22,27 +22,42 @@ <% end %> <div class="mb-4"> - <%= f.label :password, class: "block text-gray-700 dark:text-gray-300 mb-2" %> - <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">(leave blank if you don't want to change it)</p> - <%= f.password_field :password, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> - <% if @minimum_password_length %> - <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"><%= @minimum_password_length %> characters minimum</p> - <% end %> - </div> - - <div class="mb-4"> - <%= f.label :password_confirmation, class: "block text-gray-700 dark:text-gray-300 mb-2" %> - <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> - </div> - - <div class="mb-4"> - <%= f.label :darkmode, class: "block text-gray-700 dark:text-gray-300 mb-2" %> - <%= f.select :darkmode, User.darkmodes.keys.map { |d| [d.humanize, d] }, {}, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + <button type="button" id="togglePasswordFields" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-md mb-4"> + Change Password + </button> + + <div id="passwordFields" class="hidden space-y-4 mt-4 p-4 border border-gray-200 dark:border-gray-700 rounded-md"> + <div> + <%= f.label :password, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">(leave blank if you don't want to change it)</p> + <%= f.password_field :password, autocomplete: "new-password", autocomplete: "off", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + <% if @minimum_password_length %> + <p class="text-sm text-gray-500 dark:text-gray-400 mt-1"><%= @minimum_password_length %> characters minimum</p> + <% end %> + </div> + + <div> + <%= f.label :password_confirmation, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + </div> </div> <div class="mb-4"> - <%= f.label :avatar_color, class: "block text-gray-700 dark:text-gray-300 mb-2" %> - <%= f.color_field :avatar_color, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" %> + <h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-3">Appearance Settings</h3> + <div class="flex flex-col md:flex-row gap-4"> + <div class="flex-1"> + <%= f.label :darkmode, "Theme Preference", class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.select :darkmode, User.darkmodes.keys.map { |d| [d.humanize, d] }, {}, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Choose your preferred theme appearance</p> + </div> + + <div class="flex-1"> + <%= f.label :avatar_color, "Profile Color", class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.color_field :avatar_color, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" %> + <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Select a color for your avatar/profile</p> + </div> + </div> </div> <div class="hidden"> @@ -50,21 +65,29 @@ <%= f.text_field :telegram_username, class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> </div> - <fieldset class="mb-4 border border-gray-300 dark:border-gray-600 p-4 rounded-md"> - <legend class="text-lg font-semibold text-gray-700 dark:text-gray-300 px-2">More Languages Team Only</legend> - <p class="text-sm text-gray-500 dark:text-gray-400 mb-2">Please use comma-separated two-letter codes.</p> - <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Leave empty unless you are with the more languages team.</p> - - <div class="mb-4"> - <%= f.label :languages_from, class: "block text-gray-700 dark:text-gray-300 mb-2" %> - <%= f.text_field :languages_from, placeholder: "de,en", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> - </div> - - <div class="mb-4"> - <%= f.label :languages_to, class: "block text-gray-700 dark:text-gray-300 mb-2" %> - <%= f.text_field :languages_to, placeholder: "jp,es", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + <div class="mb-4"> + <button type="button" id="toggleLanguagesFields" class="flex items-center justify-between w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 rounded-md mb-4"> + <span class="text-lg font-semibold">More Languages Team Only</span> + <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 transform transition-transform" viewBox="0 0 20 20" fill="currentColor" id="languagesArrow"> + <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> + </svg> + </button> + + <div id="languagesFields" class="hidden space-y-4 mt-4 p-4 border border-gray-200 dark:border-gray-700 rounded-md"> + <p class="text-sm text-gray-500 dark:text-gray-400">Please use comma-separated two-letter codes.</p> + <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Leave empty unless you are with the more languages team.</p> + + <div class="mb-4"> + <%= f.label :languages_from, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :languages_from, placeholder: "de,en", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> + + <div class="mb-4"> + <%= f.label :languages_to, class: "block text-gray-700 dark:text-gray-300 mb-2" %> + <%= f.text_field :languages_to, placeholder: "jp,es", class: "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" %> + </div> </div> - </fieldset> + </div> <div class="mb-6"> <%= f.label :current_password, class: "block text-gray-700 dark:text-gray-300 mb-2" %> @@ -84,3 +107,33 @@ <%= link_to "Back", :back, class: "inline-block text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 mt-4" %> </div> </div> + +<script> + // Toggle password fields + document.addEventListener('DOMContentLoaded', function() { + const togglePasswordBtn = document.getElementById('togglePasswordFields'); + const passwordFields = document.getElementById('passwordFields'); + + togglePasswordBtn.addEventListener('click', function() { + passwordFields.classList.toggle('hidden'); + }); + + // Toggle languages fields + const toggleLanguagesBtn = document.getElementById('toggleLanguagesFields'); + const languagesFields = document.getElementById('languagesFields'); + const languagesArrow = document.getElementById('languagesArrow'); + + toggleLanguagesBtn.addEventListener('click', function() { + languagesFields.classList.toggle('hidden'); + languagesArrow.classList.toggle('rotate-180'); + }); + + // Disable autocomplete for password fields + const passwordInputs = document.querySelectorAll('input[type="password"]'); + passwordInputs.forEach(input => { + input.setAttribute('autocomplete', 'new-password'); + // Add a random name attribute to further prevent autocomplete + input.setAttribute('name', input.getAttribute('name') + '_' + Math.random().toString(36).substring(2)); + }); + }); +</script> diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb new file mode 100644 index 0000000..22d45dd --- /dev/null +++ b/app/views/devise/shared/_error_messages.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> + <div id="error_explanation" data-turbo-cache="false" class="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200 px-4 py-3 rounded mb-4"> + <h2 class="text-lg font-medium"> + <%= I18n.t("errors.messages.not_saved", + count: resource.errors.count, + resource: resource.class.model_name.human.downcase) + %> + </h2> + <ul class="list-disc list-inside mt-2"> + <% resource.errors.full_messages.each do |message| %> + <li><%= message %></li> + <% end %> + </ul> + </div> +<% end %> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 0000000..ffc34de --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +<h2>Resend unlock instructions</h2> + +<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + <%= render "devise/shared/error_messages", resource: resource %> + + <div class="field"> + <%= f.label :email %><br /> + <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + </div> + + <div class="actions"> + <%= f.submit "Resend unlock instructions" %> + </div> +<% end %> + +<%= render "devise/shared/links" %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2d1681d..c6c4711 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,5 +1,5 @@ <!DOCTYPE html> -<html class="<%= "dark" if current_user&.darkmode == "dark"%>"> +<html class="<%= current_user&.darkmode || "auto" %>"> <head> <title>re:scheduled</title> <meta name="viewport" content="width=device-width,initial-scale=1"> @@ -9,6 +9,26 @@ <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> + + <script> + (function() { + if (!document.documentElement.classList.contains('dark') && + !document.documentElement.classList.contains('light')) { + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.add('light'); + } + } + + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + if (!localStorage.getItem('theme')) { + document.documentElement.classList.remove('dark', 'light'); + document.documentElement.classList.add(e.matches ? 'dark' : 'light'); + } + }); + })(); + </script> </head> <body <%= tag.attributes(body_data_attributes) %> class="bg-gray-100 dark:bg-gray-900 min-h-screen"> @@ -41,9 +61,9 @@ </div> <% else %> <div class="flex items-center space-x-4"> - <span class="px-2 text-gray-600 dark:text-gray-400">not logged in</span> - <%= 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" %> + <span class="px-2 text-gray-600 dark:text-gray-300">not logged in</span> + <%= link_to "Log in", new_user_session_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> + <%= link_to "Sign Up", new_user_registration_path, class: "text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white" %> </div> <% end %> </nav> @@ -52,17 +72,19 @@ <main> <% if notice %> - <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4" role="alert"> + <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4 + dark:bg-green-900 dark:border-green-600 dark:text-green-300" role="alert"> <span class="block sm:inline"><%= notice %></span> </div> <% end %> - + <% if alert %> - <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4" role="alert"> + <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4 mx-4 mt-4 + dark:bg-red-900 dark:border-red-600 dark:text-red-300" role="alert"> <span class="block sm:inline"><%= alert %></span> </div> <% end %> - + <%= yield %> </main> </body> -- GitLab