diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index b16e53d6..d9fb144e 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,3 +1,4 @@ //= link_tree ../images //= link_directory ../javascripts .js //= link_directory ../stylesheets .css +//= link settings.js diff --git a/app/assets/javascripts/settings.js b/app/assets/javascripts/settings.js new file mode 100644 index 00000000..88210c33 --- /dev/null +++ b/app/assets/javascripts/settings.js @@ -0,0 +1,360 @@ +// Settings page JavaScript functionality +document.addEventListener('DOMContentLoaded', function() { + // Toggle switches + initializeToggleSwitches(); + + // Form validation + initializeFormValidation(); + + // Password strength meter + initializePasswordStrength(); + + // Toast notifications + initializeToasts(); + + // Mobile navigation + initializeMobileNav(); + + // Tooltips + initializeTooltips(); +}); + +// Initialize toggle switches to replace checkboxes +function initializeToggleSwitches() { + document.querySelectorAll('.toggle-switch').forEach(function(toggle) { + toggle.addEventListener('click', function(e) { + if (e.target.tagName !== 'INPUT') { + const checkbox = this.querySelector('input[type="checkbox"]'); + checkbox.checked = !checkbox.checked; + + // Trigger change event for any listeners + const event = new Event('change', { bubbles: true }); + checkbox.dispatchEvent(event); + + // Update toggle appearance + updateToggleState(checkbox); + } + }); + + // Set initial state + const checkbox = toggle.querySelector('input[type="checkbox"]'); + updateToggleState(checkbox); + + // Listen for changes + checkbox.addEventListener('change', function() { + updateToggleState(this); + }); + }); +} + +// Update toggle switch appearance based on checkbox state +function updateToggleState(checkbox) { + const toggle = checkbox.closest('.toggle-switch'); + const toggleButton = toggle.querySelector('.toggle-dot'); + + if (checkbox.checked) { + toggle.classList.add('bg-notebook-blue'); + toggle.classList.remove('bg-gray-200'); + toggleButton.classList.add('translate-x-5'); + toggleButton.classList.remove('translate-x-0'); + } else { + toggle.classList.remove('bg-notebook-blue'); + toggle.classList.add('bg-gray-200'); + toggleButton.classList.remove('translate-x-5'); + toggleButton.classList.add('translate-x-0'); + } +} + +// Initialize form validation +function initializeFormValidation() { + // Username validation + const usernameField = document.getElementById('user_username'); + if (usernameField) { + usernameField.addEventListener('input', function() { + validateUsername(this); + }); + + // Initial validation + if (usernameField.value) { + validateUsername(usernameField); + } + } + + // Email validation + const emailField = document.getElementById('user_email'); + if (emailField) { + emailField.addEventListener('input', function() { + validateEmail(this); + }); + + // Initial validation + if (emailField.value) { + validateEmail(emailField); + } + } + + // Character counters + document.querySelectorAll('[data-max-length]').forEach(function(element) { + const maxLength = element.getAttribute('data-max-length'); + const counter = document.createElement('div'); + counter.className = 'text-xs text-right text-gray-500 mt-1'; + counter.innerHTML = `${element.value.length}/${maxLength}`; + element.parentNode.appendChild(counter); + + element.addEventListener('input', function() { + const currentLength = this.value.length; + const counterElement = this.parentNode.querySelector('.current-length'); + counterElement.textContent = currentLength; + + if (currentLength > maxLength) { + counterElement.classList.add('text-red-500'); + } else { + counterElement.classList.remove('text-red-500'); + } + }); + }); +} + +// Validate username +function validateUsername(field) { + const username = field.value; + const feedbackElement = field.parentNode.querySelector('.validation-feedback') || createFeedbackElement(field); + + // Clear previous validation classes + field.classList.remove('border-red-300', 'border-green-300', 'focus:border-red-300', 'focus:border-green-300'); + + if (username.length === 0) { + feedbackElement.textContent = ''; + return; + } + + // Simple validation - can be expanded + const validUsernameRegex = /^[a-zA-Z0-9_\-$+!*]{1,40}$/; + + if (validUsernameRegex.test(username)) { + field.classList.add('border-green-300', 'focus:border-green-300'); + feedbackElement.textContent = 'Username is valid'; + feedbackElement.className = 'validation-feedback text-xs text-green-600 mt-1'; + } else { + field.classList.add('border-red-300', 'focus:border-red-300'); + feedbackElement.textContent = 'Username can only contain letters, numbers, and - _ $ + ! *'; + feedbackElement.className = 'validation-feedback text-xs text-red-600 mt-1'; + } +} + +// Validate email +function validateEmail(field) { + const email = field.value; + const feedbackElement = field.parentNode.querySelector('.validation-feedback') || createFeedbackElement(field); + + // Clear previous validation classes + field.classList.remove('border-red-300', 'border-green-300', 'focus:border-red-300', 'focus:border-green-300'); + + if (email.length === 0) { + feedbackElement.textContent = ''; + return; + } + + // Simple email validation + const validEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (validEmailRegex.test(email)) { + field.classList.add('border-green-300', 'focus:border-green-300'); + feedbackElement.textContent = 'Email is valid'; + feedbackElement.className = 'validation-feedback text-xs text-green-600 mt-1'; + } else { + field.classList.add('border-red-300', 'focus:border-red-300'); + feedbackElement.textContent = 'Please enter a valid email address'; + feedbackElement.className = 'validation-feedback text-xs text-red-600 mt-1'; + } +} + +// Create feedback element for validation +function createFeedbackElement(field) { + const feedbackElement = document.createElement('div'); + feedbackElement.className = 'validation-feedback text-xs mt-1'; + field.parentNode.appendChild(feedbackElement); + return feedbackElement; +} + +// Initialize password strength meter +function initializePasswordStrength() { + const passwordField = document.getElementById('user_password'); + if (!passwordField) return; + + // Create strength meter + const strengthMeter = document.createElement('div'); + strengthMeter.className = 'password-strength mt-2 hidden'; + strengthMeter.innerHTML = ` +
+
+
+
+
+
+

Password strength

+ `; + + passwordField.parentNode.appendChild(strengthMeter); + + passwordField.addEventListener('input', function() { + const password = this.value; + + if (password.length > 0) { + strengthMeter.classList.remove('hidden'); + updatePasswordStrength(password, strengthMeter); + } else { + strengthMeter.classList.add('hidden'); + } + }); +} + +// Update password strength meter +function updatePasswordStrength(password, meterElement) { + // Calculate password strength (simplified version) + let strength = 0; + + if (password.length >= 8) strength++; + if (password.match(/[a-z]/) && password.match(/[A-Z]/)) strength++; + if (password.match(/\d/)) strength++; + if (password.match(/[^a-zA-Z\d]/)) strength++; + + // Update strength meter + const strengthBars = meterElement.querySelectorAll('[data-strength]'); + const strengthText = meterElement.querySelector('.strength-text'); + + strengthBars.forEach(bar => { + const barStrength = parseInt(bar.getAttribute('data-strength')); + + if (barStrength <= strength) { + bar.classList.remove('bg-gray-200'); + + if (strength === 1) bar.classList.add('bg-red-500'); + else if (strength === 2) bar.classList.add('bg-orange-500'); + else if (strength === 3) bar.classList.add('bg-yellow-500'); + else bar.classList.add('bg-green-500'); + } else { + bar.className = 'h-1 w-1/4 rounded-full bg-gray-200'; + } + }); + + // Update text + if (strength === 0) strengthText.textContent = 'Password is too weak'; + else if (strength === 1) strengthText.textContent = 'Password is weak'; + else if (strength === 2) strengthText.textContent = 'Password is fair'; + else if (strength === 3) strengthText.textContent = 'Password is good'; + else strengthText.textContent = 'Password is strong'; + + // Update text color + strengthText.className = 'text-xs mt-1 '; + if (strength === 0 || strength === 1) strengthText.className += 'text-red-500'; + else if (strength === 2) strengthText.className += 'text-orange-500'; + else if (strength === 3) strengthText.className += 'text-yellow-600'; + else strengthText.className += 'text-green-600'; +} + +// Initialize toast notifications +function initializeToasts() { + window.showToast = function(message, type = 'success') { + const toast = document.createElement('div'); + toast.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-lg shadow-lg transform transition-all duration-500 translate-y-20 opacity-0 z-50'; + + // Set color based on type + if (type === 'success') { + toast.classList.add('bg-green-500', 'text-white'); + } else if (type === 'error') { + toast.classList.add('bg-red-500', 'text-white'); + } else if (type === 'info') { + toast.classList.add('bg-blue-500', 'text-white'); + } + + toast.textContent = message; + document.body.appendChild(toast); + + // Show toast + setTimeout(() => { + toast.classList.remove('translate-y-20', 'opacity-0'); + }, 100); + + // Hide toast after 3 seconds + setTimeout(() => { + toast.classList.add('translate-y-20', 'opacity-0'); + + // Remove from DOM after animation + setTimeout(() => { + document.body.removeChild(toast); + }, 500); + }, 3000); + }; + + // Add event listeners to forms + document.querySelectorAll('form').forEach(form => { + form.addEventListener('submit', function() { + // Store a flag in localStorage to show toast after redirect + localStorage.setItem('showSettingsSavedToast', 'true'); + }); + }); + + // Check if we need to show a toast (after redirect) + if (localStorage.getItem('showSettingsSavedToast') === 'true') { + showToast('Settings saved successfully!', 'success'); + localStorage.removeItem('showSettingsSavedToast'); + } +} + +// Initialize mobile navigation +function initializeMobileNav() { + const mobileNavToggle = document.getElementById('mobile-nav-toggle'); + const settingsSidebar = document.querySelector('.settings-sidebar-mobile'); + + if (mobileNavToggle && settingsSidebar) { + mobileNavToggle.addEventListener('click', function() { + settingsSidebar.classList.toggle('translate-x-0'); + settingsSidebar.classList.toggle('-translate-x-full'); + }); + } +} + +// Initialize tooltips +function initializeTooltips() { + document.querySelectorAll('[data-tooltip]').forEach(element => { + const tooltipText = element.getAttribute('data-tooltip'); + + element.addEventListener('mouseenter', function(e) { + const tooltip = document.createElement('div'); + tooltip.className = 'absolute z-10 px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm tooltip'; + tooltip.textContent = tooltipText; + tooltip.style.top = `${e.target.offsetTop - 40}px`; + tooltip.style.left = `${e.target.offsetLeft + (e.target.offsetWidth / 2) - 80}px`; + + document.body.appendChild(tooltip); + + // Position tooltip + const rect = tooltip.getBoundingClientRect(); + if (rect.left < 0) { + tooltip.style.left = '0px'; + } else if (rect.right > window.innerWidth) { + tooltip.style.left = `${window.innerWidth - rect.width - 10}px`; + } + + // Add arrow + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + arrow.style.position = 'absolute'; + arrow.style.width = '10px'; + arrow.style.height = '10px'; + arrow.style.background = '#1F2937'; + arrow.style.transform = 'rotate(45deg)'; + arrow.style.bottom = '-5px'; + arrow.style.left = 'calc(50% - 5px)'; + tooltip.appendChild(arrow); + }); + + element.addEventListener('mouseleave', function() { + const tooltip = document.querySelector('.tooltip'); + if (tooltip) { + document.body.removeChild(tooltip); + } + }); + }); +} \ No newline at end of file diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 656f4157..37d5c38a 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -5,7 +5,15 @@ class RegistrationsController < Devise::RegistrationsController before_action :set_navbar_actions, only: [:edit, :preferences, :more_actions] before_action :set_navbar_color, only: [:edit, :preferences, :more_actions] - layout 'tailwind/landing', only: [:new, :create] + layout :determine_layout + + def determine_layout + if action_name.in?(['new', 'create']) + 'tailwind/landing' + else + 'tailwind' + end + end def new super diff --git a/app/javascript/settings.js b/app/javascript/settings.js new file mode 100644 index 00000000..88210c33 --- /dev/null +++ b/app/javascript/settings.js @@ -0,0 +1,360 @@ +// Settings page JavaScript functionality +document.addEventListener('DOMContentLoaded', function() { + // Toggle switches + initializeToggleSwitches(); + + // Form validation + initializeFormValidation(); + + // Password strength meter + initializePasswordStrength(); + + // Toast notifications + initializeToasts(); + + // Mobile navigation + initializeMobileNav(); + + // Tooltips + initializeTooltips(); +}); + +// Initialize toggle switches to replace checkboxes +function initializeToggleSwitches() { + document.querySelectorAll('.toggle-switch').forEach(function(toggle) { + toggle.addEventListener('click', function(e) { + if (e.target.tagName !== 'INPUT') { + const checkbox = this.querySelector('input[type="checkbox"]'); + checkbox.checked = !checkbox.checked; + + // Trigger change event for any listeners + const event = new Event('change', { bubbles: true }); + checkbox.dispatchEvent(event); + + // Update toggle appearance + updateToggleState(checkbox); + } + }); + + // Set initial state + const checkbox = toggle.querySelector('input[type="checkbox"]'); + updateToggleState(checkbox); + + // Listen for changes + checkbox.addEventListener('change', function() { + updateToggleState(this); + }); + }); +} + +// Update toggle switch appearance based on checkbox state +function updateToggleState(checkbox) { + const toggle = checkbox.closest('.toggle-switch'); + const toggleButton = toggle.querySelector('.toggle-dot'); + + if (checkbox.checked) { + toggle.classList.add('bg-notebook-blue'); + toggle.classList.remove('bg-gray-200'); + toggleButton.classList.add('translate-x-5'); + toggleButton.classList.remove('translate-x-0'); + } else { + toggle.classList.remove('bg-notebook-blue'); + toggle.classList.add('bg-gray-200'); + toggleButton.classList.remove('translate-x-5'); + toggleButton.classList.add('translate-x-0'); + } +} + +// Initialize form validation +function initializeFormValidation() { + // Username validation + const usernameField = document.getElementById('user_username'); + if (usernameField) { + usernameField.addEventListener('input', function() { + validateUsername(this); + }); + + // Initial validation + if (usernameField.value) { + validateUsername(usernameField); + } + } + + // Email validation + const emailField = document.getElementById('user_email'); + if (emailField) { + emailField.addEventListener('input', function() { + validateEmail(this); + }); + + // Initial validation + if (emailField.value) { + validateEmail(emailField); + } + } + + // Character counters + document.querySelectorAll('[data-max-length]').forEach(function(element) { + const maxLength = element.getAttribute('data-max-length'); + const counter = document.createElement('div'); + counter.className = 'text-xs text-right text-gray-500 mt-1'; + counter.innerHTML = `${element.value.length}/${maxLength}`; + element.parentNode.appendChild(counter); + + element.addEventListener('input', function() { + const currentLength = this.value.length; + const counterElement = this.parentNode.querySelector('.current-length'); + counterElement.textContent = currentLength; + + if (currentLength > maxLength) { + counterElement.classList.add('text-red-500'); + } else { + counterElement.classList.remove('text-red-500'); + } + }); + }); +} + +// Validate username +function validateUsername(field) { + const username = field.value; + const feedbackElement = field.parentNode.querySelector('.validation-feedback') || createFeedbackElement(field); + + // Clear previous validation classes + field.classList.remove('border-red-300', 'border-green-300', 'focus:border-red-300', 'focus:border-green-300'); + + if (username.length === 0) { + feedbackElement.textContent = ''; + return; + } + + // Simple validation - can be expanded + const validUsernameRegex = /^[a-zA-Z0-9_\-$+!*]{1,40}$/; + + if (validUsernameRegex.test(username)) { + field.classList.add('border-green-300', 'focus:border-green-300'); + feedbackElement.textContent = 'Username is valid'; + feedbackElement.className = 'validation-feedback text-xs text-green-600 mt-1'; + } else { + field.classList.add('border-red-300', 'focus:border-red-300'); + feedbackElement.textContent = 'Username can only contain letters, numbers, and - _ $ + ! *'; + feedbackElement.className = 'validation-feedback text-xs text-red-600 mt-1'; + } +} + +// Validate email +function validateEmail(field) { + const email = field.value; + const feedbackElement = field.parentNode.querySelector('.validation-feedback') || createFeedbackElement(field); + + // Clear previous validation classes + field.classList.remove('border-red-300', 'border-green-300', 'focus:border-red-300', 'focus:border-green-300'); + + if (email.length === 0) { + feedbackElement.textContent = ''; + return; + } + + // Simple email validation + const validEmailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (validEmailRegex.test(email)) { + field.classList.add('border-green-300', 'focus:border-green-300'); + feedbackElement.textContent = 'Email is valid'; + feedbackElement.className = 'validation-feedback text-xs text-green-600 mt-1'; + } else { + field.classList.add('border-red-300', 'focus:border-red-300'); + feedbackElement.textContent = 'Please enter a valid email address'; + feedbackElement.className = 'validation-feedback text-xs text-red-600 mt-1'; + } +} + +// Create feedback element for validation +function createFeedbackElement(field) { + const feedbackElement = document.createElement('div'); + feedbackElement.className = 'validation-feedback text-xs mt-1'; + field.parentNode.appendChild(feedbackElement); + return feedbackElement; +} + +// Initialize password strength meter +function initializePasswordStrength() { + const passwordField = document.getElementById('user_password'); + if (!passwordField) return; + + // Create strength meter + const strengthMeter = document.createElement('div'); + strengthMeter.className = 'password-strength mt-2 hidden'; + strengthMeter.innerHTML = ` +
+
+
+
+
+
+

Password strength

+ `; + + passwordField.parentNode.appendChild(strengthMeter); + + passwordField.addEventListener('input', function() { + const password = this.value; + + if (password.length > 0) { + strengthMeter.classList.remove('hidden'); + updatePasswordStrength(password, strengthMeter); + } else { + strengthMeter.classList.add('hidden'); + } + }); +} + +// Update password strength meter +function updatePasswordStrength(password, meterElement) { + // Calculate password strength (simplified version) + let strength = 0; + + if (password.length >= 8) strength++; + if (password.match(/[a-z]/) && password.match(/[A-Z]/)) strength++; + if (password.match(/\d/)) strength++; + if (password.match(/[^a-zA-Z\d]/)) strength++; + + // Update strength meter + const strengthBars = meterElement.querySelectorAll('[data-strength]'); + const strengthText = meterElement.querySelector('.strength-text'); + + strengthBars.forEach(bar => { + const barStrength = parseInt(bar.getAttribute('data-strength')); + + if (barStrength <= strength) { + bar.classList.remove('bg-gray-200'); + + if (strength === 1) bar.classList.add('bg-red-500'); + else if (strength === 2) bar.classList.add('bg-orange-500'); + else if (strength === 3) bar.classList.add('bg-yellow-500'); + else bar.classList.add('bg-green-500'); + } else { + bar.className = 'h-1 w-1/4 rounded-full bg-gray-200'; + } + }); + + // Update text + if (strength === 0) strengthText.textContent = 'Password is too weak'; + else if (strength === 1) strengthText.textContent = 'Password is weak'; + else if (strength === 2) strengthText.textContent = 'Password is fair'; + else if (strength === 3) strengthText.textContent = 'Password is good'; + else strengthText.textContent = 'Password is strong'; + + // Update text color + strengthText.className = 'text-xs mt-1 '; + if (strength === 0 || strength === 1) strengthText.className += 'text-red-500'; + else if (strength === 2) strengthText.className += 'text-orange-500'; + else if (strength === 3) strengthText.className += 'text-yellow-600'; + else strengthText.className += 'text-green-600'; +} + +// Initialize toast notifications +function initializeToasts() { + window.showToast = function(message, type = 'success') { + const toast = document.createElement('div'); + toast.className = 'fixed bottom-4 right-4 px-4 py-2 rounded-lg shadow-lg transform transition-all duration-500 translate-y-20 opacity-0 z-50'; + + // Set color based on type + if (type === 'success') { + toast.classList.add('bg-green-500', 'text-white'); + } else if (type === 'error') { + toast.classList.add('bg-red-500', 'text-white'); + } else if (type === 'info') { + toast.classList.add('bg-blue-500', 'text-white'); + } + + toast.textContent = message; + document.body.appendChild(toast); + + // Show toast + setTimeout(() => { + toast.classList.remove('translate-y-20', 'opacity-0'); + }, 100); + + // Hide toast after 3 seconds + setTimeout(() => { + toast.classList.add('translate-y-20', 'opacity-0'); + + // Remove from DOM after animation + setTimeout(() => { + document.body.removeChild(toast); + }, 500); + }, 3000); + }; + + // Add event listeners to forms + document.querySelectorAll('form').forEach(form => { + form.addEventListener('submit', function() { + // Store a flag in localStorage to show toast after redirect + localStorage.setItem('showSettingsSavedToast', 'true'); + }); + }); + + // Check if we need to show a toast (after redirect) + if (localStorage.getItem('showSettingsSavedToast') === 'true') { + showToast('Settings saved successfully!', 'success'); + localStorage.removeItem('showSettingsSavedToast'); + } +} + +// Initialize mobile navigation +function initializeMobileNav() { + const mobileNavToggle = document.getElementById('mobile-nav-toggle'); + const settingsSidebar = document.querySelector('.settings-sidebar-mobile'); + + if (mobileNavToggle && settingsSidebar) { + mobileNavToggle.addEventListener('click', function() { + settingsSidebar.classList.toggle('translate-x-0'); + settingsSidebar.classList.toggle('-translate-x-full'); + }); + } +} + +// Initialize tooltips +function initializeTooltips() { + document.querySelectorAll('[data-tooltip]').forEach(element => { + const tooltipText = element.getAttribute('data-tooltip'); + + element.addEventListener('mouseenter', function(e) { + const tooltip = document.createElement('div'); + tooltip.className = 'absolute z-10 px-3 py-2 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm tooltip'; + tooltip.textContent = tooltipText; + tooltip.style.top = `${e.target.offsetTop - 40}px`; + tooltip.style.left = `${e.target.offsetLeft + (e.target.offsetWidth / 2) - 80}px`; + + document.body.appendChild(tooltip); + + // Position tooltip + const rect = tooltip.getBoundingClientRect(); + if (rect.left < 0) { + tooltip.style.left = '0px'; + } else if (rect.right > window.innerWidth) { + tooltip.style.left = `${window.innerWidth - rect.width - 10}px`; + } + + // Add arrow + const arrow = document.createElement('div'); + arrow.className = 'tooltip-arrow'; + arrow.style.position = 'absolute'; + arrow.style.width = '10px'; + arrow.style.height = '10px'; + arrow.style.background = '#1F2937'; + arrow.style.transform = 'rotate(45deg)'; + arrow.style.bottom = '-5px'; + arrow.style.left = 'calc(50% - 5px)'; + tooltip.appendChild(arrow); + }); + + element.addEventListener('mouseleave', function() { + const tooltip = document.querySelector('.tooltip'); + if (tooltip) { + document.body.removeChild(tooltip); + } + }); + }); +} \ No newline at end of file diff --git a/app/views/devise/registrations/_settings_sidebar.html.erb b/app/views/devise/registrations/_settings_sidebar.html.erb new file mode 100644 index 00000000..535d75d4 --- /dev/null +++ b/app/views/devise/registrations/_settings_sidebar.html.erb @@ -0,0 +1,74 @@ +
+
+
+ <%= image_tag current_user.image_url(size=40), class: "h-10 w-10 rounded-full" %> +
+

<%= current_user.name %>

+

<%= current_user.email %>

+
+
+
+ + + +
+
+
+
+ + + +
+
+

Need help?

+
+

If you need assistance with your account settings, please contact support.

+
+
+
+
+
+
\ No newline at end of file diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb index 1e5fd0ce..71d8ade7 100644 --- a/app/views/devise/registrations/edit.html.erb +++ b/app/views/devise/registrations/edit.html.erb @@ -1,25 +1,103 @@ -<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %> - <% if resource.errors.any? %> -
-
-
Please fix the following errors:
- +
+
+
+
+
+

<%= @page_title || "Account Settings" %>

+

Manage your account settings and preferences

+
- <% end %> - -
-
- <%= render partial: 'devise/registrations/panes/information', locals: { f: f } %> + +
+ + + + +
+ +
+ + +
+ <%= render 'devise/registrations/settings_sidebar' %> +
+ + +
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, id: "settings-form" }) do |f| %> + <% if resource.errors.any? %> +
+
+
+
+ + + +
+
+

Please fix the following errors:

+
+
    + <%= devise_error_messages! %> +
+
+
+
+
+
+ <% end %> + +
+
+

About You

+

Update your personal information and how others see you on Notebook.ai.

+
+ + <%= render partial: 'devise/registrations/panes/information', locals: { f: f } %> +
+ +
+
+ <%= link_to "Cancel", :back, class: 'px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue' %> + <%= f.submit "Save changes", class: 'inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-notebook-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue' %> +
+
+ <% end %> +
+
-
-
- <%= f.submit "Save changes", class: 'btn blue' %> - <%= link_to "Cancel", :back, class: 'btn grey' %> -
-
-<% end %> + diff --git a/app/views/devise/registrations/more_actions.html.erb b/app/views/devise/registrations/more_actions.html.erb index 061cc567..52238a99 100644 --- a/app/views/devise/registrations/more_actions.html.erb +++ b/app/views/devise/registrations/more_actions.html.erb @@ -1,25 +1,103 @@ -<%= form_for(current_user, as: :user, url: registration_path(:user), html: { method: :put }) do |f| %> - <% if current_user.errors.any? %> -
-
-
Please fix the following errors:
-
    - <%= devise_error_messages! %> -
+
+
+
+
+
+

<%= @page_title || "Account Settings" %>

+

Manage your account settings and preferences

+
- <% end %> - -
-
- <%= render partial: 'devise/registrations/panes/more', locals: { f: f } %> + +
+ + + + +
+ +
+ + +
+ <%= render 'devise/registrations/settings_sidebar' %> +
+ + +
+ <%= form_for(current_user, as: :user, url: registration_path(:user), html: { method: :put, id: "settings-form" }) do |f| %> + <% if current_user.errors.any? %> +
+
+
+
+ + + +
+
+

Please fix the following errors:

+
+
    + <%= devise_error_messages! %> +
+
+
+
+
+
+ <% end %> + +
+
+

Account Actions

+

Manage your account and take important actions.

+
+ + <%= render partial: 'devise/registrations/panes/more', locals: { f: f } %> +
+ +
+
+ <%= link_to "Cancel", :back, class: 'px-4 py-2 bg-white border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue' %> + <%= f.submit "Save changes", class: 'inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-notebook-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue' %> +
+
+ <% end %> +
+
-
-
- <%= f.submit "Save changes", class: 'btn blue' %> - <%= link_to "Cancel", :back, class: 'btn grey' %> -
-
-<% end %> + diff --git a/app/views/devise/registrations/panes/_information.html.erb b/app/views/devise/registrations/panes/_information.html.erb index 446652ef..f0712a08 100644 --- a/app/views/devise/registrations/panes/_information.html.erb +++ b/app/views/devise/registrations/panes/_information.html.erb @@ -1,208 +1,273 @@ -
-
-

Public information

-

- This information is visible to other users on <%= link_to 'your profile', current_user %>. -

-
-
-
-
-
- <%= f.label 'Name (visible on your profile and forum posts)' %>
- <%= f.text_field :name, autofocus: true %> -
- - <%= f.label 'Username' %>
-
- alternate_email - <%= f.text_field :username, data: { length: 40 }, placeholder: 'your-username-here', class: 'with-character-counter' %> - Your Notebook.ai profile will be available at https://www.notebook.ai/@username.
- Up to 40 numbers, letters, and/or the following symbols are allowed: - _ $ + ! * +
+ +
+
+

Public information

+

This information is visible to other users on <%= link_to 'your profile', current_user, class: "text-notebook-blue hover:text-blue-700" %>.

+
+ +
+ +
+ +
+ <%= f.text_field :name, autofocus: true, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %>
-
- -
-
-
-
- -

- <%= image_tag current_user.image_url(size=160), class: 'hoverable materialboxed' %> + +
+ +
+
+ @
-
- -
-
- Upload - <%= f.file_field :avatar %> -
-
- -
-
-
- Supported file types: .png, .jpg, .jpeg, .gif -
-
- After selecting an image here, submit your changes with the usual "Save changes" button at the bottom of this page to save it. -
+ <%= f.text_field :username, data: { max_length: 40 }, placeholder: 'your-username-here', class: "pl-8 shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+

Your Notebook.ai profile will be available at https://www.notebook.ai/@username.

+

Up to 40 numbers, letters, and/or the following symbols are allowed: - _ $ + ! *

+
+
+
+ + +
+
+

Profile picture

+

Your avatar is displayed on your profile and next to your posts

+
+ +
+
+
+
+ <%= image_tag current_user.image_url(size=160), class: 'h-24 w-24 rounded-full object-cover shadow-sm' %> +
+
+

Upload a new avatar

+

Supported file types: .png, .jpg, .jpeg, .gif

+

After selecting an image, submit your changes with the "Save changes" button to save it.

+
+
+
+
+
- -
-
-
- <%= f.text_area :bio, class: 'materialize-textarea with-character-counter', data: { length: 500 } %> - <%= f.label 'Bio' %>
-
- -
- <%= f.text_field :interests, class: 'materialize-textarea' %> - <%= f.label 'Interests' %>
-
- -
- <%= f.text_field :favorite_genre, class: 'materialize-textarea' %> - <%= f.label 'Favorite genre' %>
-
- -
- <%= f.text_field :favorite_author, class: 'materialize-textarea' %> - <%= f.label 'Favorite author' %>
-
- -
- <%= f.text_field :favorite_book, class: 'materialize-textarea' %> - <%= f.label 'Favorite book' %>
-
- -
- <%= f.text_field :favorite_quote, class: 'materialize-textarea' %> - <%= f.label 'Favorite quote' %>
-
- -
- <%= f.text_field :inspirations, class: 'materialize-textarea' %> - <%= f.label 'Inspirations' %>
-
- -
- <%= f.text_field :other_names, class: 'materialize-textarea' %> - <%= f.label 'Other names' %>
-
- -
- <%= f.text_field :website, class: 'materialize-textarea' %> - <%= f.label 'Website' %>
-
-
-
- -
-
-
- <%= f.label 'Favorite page type' %>
- <%= f.select(:favorite_page_type, Rails.application.config.content_types[:all].map { |type| [type.name.pluralize, type.name] }, include_blank: 'None') %> - Selecting a page here will prominently display it on your profile and somewhat theme your profile page to match.
-
-
-
- -
-
-
- <%= f.label 'Custom Premium badge text' %>
- <%= f.text_field :forums_badge_text, data: { length: 20 }, class: 'with-character-counter black-text', placeholder: 'Premium Supporter' %> - Premium users can customize the badge that appears under their name on their profile and next to their name on the forums.
-
-
-
-
-
- -
-
-

Private information

-

- This information is only ever visible to you. -

-

- Notebook.ai will never share this information, but may also look at it to better understand its users. -

-
- -
-
-
-
- <%= f.label 'Email (always completely private)' %>
- <%= f.email_field :email %> + + +
+
+

Bio & interests

+

Tell others about yourself and your writing

+
+ +
+
+ +
+ <%= f.text_area :bio, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md", rows: 4, data: { max_length: 500 } %>
-
- -
-
-
- <%= f.text_field :age, class: 'materialize-textarea' %> - <%= f.label 'Age' %>
-
- -
- <%= f.text_field :location, class: 'materialize-textarea' %> - <%= f.label 'Location' %>
-
- -
- <%= f.text_field :gender, class: 'materialize-textarea' %> - <%= f.label 'Gender' %>
-
- -
- <%= f.text_field :occupation, class: 'materialize-textarea' %> - <%= f.label 'Occupation' %>
+ +
+ +
+ <%= f.text_field :interests, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %>
-
- -
-
- Last login - <%= time_ago_in_words current_user.current_sign_in_at %> ago - from IP - <%= current_user.current_sign_in_ip %>. + +
+ +
+ <%= f.text_field :favorite_genre, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :favorite_author, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :favorite_book, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :favorite_quote, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :inspirations, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :other_names, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+
+ + + +
+ <%= f.text_field :website, class: "pl-10 shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md", placeholder: "https://yourwebsite.com" %> +
-
- -<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> -
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
-<% end %> - -
-
-

Password change

-

- Leave this blank unless you want to change your password. -

-
-
-
-
-
- <%= f.label 'New password' %>
- <%= f.password_field :password, autocomplete: "off" %> + + +
+
+

Profile customization

+

Customize how your profile appears to others

+
+ +
+
+ +
+ <%= f.select(:favorite_page_type, + Rails.application.config.content_types[:all].map { |type| [type.name.pluralize, type.name] }, + { include_blank: 'None' }, + { class: "mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-notebook-blue focus:border-notebook-blue sm:text-sm rounded-md" }) %>
- -
- <%= f.label 'New password (again)' %>
- <%= f.password_field :password_confirmation, autocomplete: "off" %> +

Selecting a page here will prominently display it on your profile and somewhat theme your profile page to match.

+
+ +
+ +
+ <%= f.text_field :forums_badge_text, data: { max_length: 20 }, placeholder: 'Premium Supporter', class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+

Premium users can customize the badge that appears under their name on their profile and next to their name on the forums.

+
+
+
+ + +
+
+

Private information

+

This information is only ever visible to you

+
+ +
+
+ +
+ <%= f.email_field :email, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :age, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :location, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :gender, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.text_field :occupation, class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+
+
+ + + +
+
+

+ Last login <%= time_ago_in_words current_user.current_sign_in_at %> ago + from IP + <%= current_user.current_sign_in_ip %> +

+
+
+
+
+
+ + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
+
+
+ + + +
+
+

+ Currently waiting confirmation for: <%= resource.unconfirmed_email %> +

+
+
+
+ <% end %> + + +
+
+

Password change

+

Leave this blank unless you want to change your password

+
+ +
+
+ +
+ <%= f.password_field :password, autocomplete: "off", class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %> +
+
+ +
+ +
+ <%= f.password_field :password_confirmation, autocomplete: "off", class: "shadow-sm focus:ring-notebook-blue focus:border-notebook-blue block w-full sm:text-sm border-gray-300 rounded-md" %>
diff --git a/app/views/devise/registrations/panes/_more.html.erb b/app/views/devise/registrations/panes/_more.html.erb index d7af92b5..fa6a6ae8 100644 --- a/app/views/devise/registrations/panes/_more.html.erb +++ b/app/views/devise/registrations/panes/_more.html.erb @@ -1,58 +1,152 @@ -
-
-

To delete your account

-

- This action is IRREVERSIBLE. Your account and all of your content - will be immediately and permanently deleted. We will miss you! -

-

-
- Delete my account - -