mirror of
https://github.com/indentlabs/notebook.git
synced 2025-10-26 11:19:22 +00:00
start on account settings redesign
This commit is contained in:
parent
e02f99d354
commit
4c07948486
@ -1,3 +1,4 @@
|
||||
//= link_tree ../images
|
||||
//= link_directory ../javascripts .js
|
||||
//= link_directory ../stylesheets .css
|
||||
//= link settings.js
|
||||
|
||||
360
app/assets/javascripts/settings.js
Normal file
360
app/assets/javascripts/settings.js
Normal file
@ -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 = `<span class="current-length">${element.value.length}</span>/${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 = `
|
||||
<div class="flex space-x-1">
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="1"></div>
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="2"></div>
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="3"></div>
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="4"></div>
|
||||
</div>
|
||||
<p class="text-xs mt-1 text-gray-500 strength-text">Password strength</p>
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -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
|
||||
|
||||
360
app/javascript/settings.js
Normal file
360
app/javascript/settings.js
Normal file
@ -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 = `<span class="current-length">${element.value.length}</span>/${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 = `
|
||||
<div class="flex space-x-1">
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="1"></div>
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="2"></div>
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="3"></div>
|
||||
<div class="h-1 w-1/4 rounded-full bg-gray-200" data-strength="4"></div>
|
||||
</div>
|
||||
<p class="text-xs mt-1 text-gray-500 strength-text">Password strength</p>
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
74
app/views/devise/registrations/_settings_sidebar.html.erb
Normal file
74
app/views/devise/registrations/_settings_sidebar.html.erb
Normal file
@ -0,0 +1,74 @@
|
||||
<div class="settings-sidebar w-full lg:w-64 bg-white border-r border-gray-200">
|
||||
<div class="p-4 border-b border-gray-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= image_tag current_user.image_url(size=40), class: "h-10 w-10 rounded-full" %>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-gray-900 truncate max-w-[180px]"><%= current_user.name %></h3>
|
||||
<p class="text-xs text-gray-500 truncate max-w-[180px]"><%= current_user.email %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="p-2 space-y-1" aria-label="Settings navigation">
|
||||
<%= link_to edit_user_registration_path, class: "flex items-center px-3 py-2 text-sm font-medium rounded-md group #{current_page?(edit_user_registration_path) ? 'bg-notebook-blue text-white' : 'text-gray-700 hover:bg-gray-50'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-3 h-5 w-5 #{current_page?(edit_user_registration_path) ? 'text-white' : 'text-gray-400 group-hover:text-gray-500'}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<span>About You</span>
|
||||
<% if current_page?(edit_user_registration_path) %>
|
||||
<span class="ml-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to user_preferences_path(current_user), class: "flex items-center px-3 py-2 text-sm font-medium rounded-md group #{current_page?(user_preferences_path(current_user)) ? 'bg-notebook-blue text-white' : 'text-gray-700 hover:bg-gray-50'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-3 h-5 w-5 #{current_page?(user_preferences_path(current_user)) ? 'text-white' : 'text-gray-400 group-hover:text-gray-500'}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span>Preferences</span>
|
||||
<% if current_page?(user_preferences_path(current_user)) %>
|
||||
<span class="ml-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to user_more_actions_path(current_user), class: "flex items-center px-3 py-2 text-sm font-medium rounded-md group #{current_page?(user_more_actions_path(current_user)) ? 'bg-notebook-blue text-white' : 'text-gray-700 hover:bg-gray-50'}" do %>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-3 h-5 w-5 #{current_page?(user_more_actions_path(current_user)) ? 'text-white' : 'text-gray-400 group-hover:text-gray-500'}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
</svg>
|
||||
<span>Account Actions</span>
|
||||
<% if current_page?(user_more_actions_path(current_user)) %>
|
||||
<span class="ml-auto">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</nav>
|
||||
|
||||
<div class="p-4 mt-6">
|
||||
<div class="rounded-md bg-blue-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">Need help?</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>If you need assistance with your account settings, please <a href="#" class="font-medium underline">contact support</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1,25 +1,103 @@
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
|
||||
<% if resource.errors.any? %>
|
||||
<div class="card red lighten-3">
|
||||
<div class="card-content">
|
||||
<div class="card-title">Please fix the following errors:</div>
|
||||
<ul class="browser-default">
|
||||
<%= devise_error_messages! %>
|
||||
</ul>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-900"><%= @page_title || "Account Settings" %></h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="row">
|
||||
<div id="your-info" class="col s12">
|
||||
<%= render partial: 'devise/registrations/panes/information', locals: { f: f } %>
|
||||
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<!-- Desktop sidebar -->
|
||||
<div class="hidden lg:block lg:w-64 lg:border-r lg:border-gray-200">
|
||||
<%= render 'devise/registrations/settings_sidebar' %>
|
||||
</div>
|
||||
|
||||
<!-- Mobile sidebar toggle -->
|
||||
<div class="lg:hidden px-4 py-3 border-b border-gray-200">
|
||||
<button id="mobile-nav-toggle" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue">
|
||||
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" 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>
|
||||
Navigation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile sidebar (hidden by default) -->
|
||||
<div class="settings-sidebar-mobile fixed inset-y-0 left-0 z-40 w-64 bg-white shadow-lg transform -translate-x-full lg:hidden transition-transform duration-300 ease-in-out pt-16">
|
||||
<%= render 'devise/registrations/settings_sidebar' %>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex-1 p-4 sm:p-6">
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, id: "settings-form" }) do |f| %>
|
||||
<% if resource.errors.any? %>
|
||||
<div class="mb-6">
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Please fix the following errors:</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<%= devise_error_messages! %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium text-gray-900">About You</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">Update your personal information and how others see you on Notebook.ai.</p>
|
||||
</div>
|
||||
|
||||
<%= render partial: 'devise/registrations/panes/information', locals: { f: f } %>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-gray-200 pt-6 flex justify-end">
|
||||
<div class="flex space-x-3">
|
||||
<%= 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' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="actions">
|
||||
<%= f.submit "Save changes", class: 'btn blue' %>
|
||||
<%= link_to "Cancel", :back, class: 'btn grey' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Mobile navigation
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile sidebar when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
if (settingsSidebar &&
|
||||
!settingsSidebar.contains(event.target) &&
|
||||
!mobileNavToggle.contains(event.target) &&
|
||||
settingsSidebar.classList.contains('translate-x-0')) {
|
||||
settingsSidebar.classList.remove('translate-x-0');
|
||||
settingsSidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,25 +1,103 @@
|
||||
<%= form_for(current_user, as: :user, url: registration_path(:user), html: { method: :put }) do |f| %>
|
||||
<% if current_user.errors.any? %>
|
||||
<div class="card red lighten-3">
|
||||
<div class="card-content">
|
||||
<div class="card-title">Please fix the following errors:</div>
|
||||
<ul class="browser-default">
|
||||
<%= devise_error_messages! %>
|
||||
</ul>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-900"><%= @page_title || "Account Settings" %></h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="row">
|
||||
<div id="preferences" class="col s12">
|
||||
<%= render partial: 'devise/registrations/panes/more', locals: { f: f } %>
|
||||
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<!-- Desktop sidebar -->
|
||||
<div class="hidden lg:block lg:w-64 lg:border-r lg:border-gray-200">
|
||||
<%= render 'devise/registrations/settings_sidebar' %>
|
||||
</div>
|
||||
|
||||
<!-- Mobile sidebar toggle -->
|
||||
<div class="lg:hidden px-4 py-3 border-b border-gray-200">
|
||||
<button id="mobile-nav-toggle" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue">
|
||||
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" 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>
|
||||
Navigation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile sidebar (hidden by default) -->
|
||||
<div class="settings-sidebar-mobile fixed inset-y-0 left-0 z-40 w-64 bg-white shadow-lg transform -translate-x-full lg:hidden transition-transform duration-300 ease-in-out pt-16">
|
||||
<%= render 'devise/registrations/settings_sidebar' %>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex-1 p-4 sm:p-6">
|
||||
<%= form_for(current_user, as: :user, url: registration_path(:user), html: { method: :put, id: "settings-form" }) do |f| %>
|
||||
<% if current_user.errors.any? %>
|
||||
<div class="mb-6">
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Please fix the following errors:</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<%= devise_error_messages! %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium text-gray-900">Account Actions</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage your account and take important actions.</p>
|
||||
</div>
|
||||
|
||||
<%= render partial: 'devise/registrations/panes/more', locals: { f: f } %>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-gray-200 pt-6 flex justify-end">
|
||||
<div class="flex space-x-3">
|
||||
<%= 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' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="actions">
|
||||
<%= f.submit "Save changes", class: 'btn blue' %>
|
||||
<%= link_to "Cancel", :back, class: 'btn grey' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Mobile navigation
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile sidebar when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
if (settingsSidebar &&
|
||||
!settingsSidebar.contains(event.target) &&
|
||||
!mobileNavToggle.contains(event.target) &&
|
||||
settingsSidebar.classList.contains('translate-x-0')) {
|
||||
settingsSidebar.classList.remove('translate-x-0');
|
||||
settingsSidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,208 +1,273 @@
|
||||
<div class="row">
|
||||
<div class="col s12 m4 grey-text">
|
||||
<h4>Public information</h4>
|
||||
<p class="flow-text">
|
||||
This information is visible to other users on <%= link_to 'your profile', current_user %>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col s12 m8">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<%= f.label 'Name (visible on your profile and forum posts)' %><br />
|
||||
<%= f.text_field :name, autofocus: true %>
|
||||
</div>
|
||||
|
||||
<%= f.label 'Username' %><br />
|
||||
<div class="input-field">
|
||||
<i class="material-icons prefix black-text">alternate_email</i>
|
||||
<%= f.text_field :username, data: { length: 40 }, placeholder: 'your-username-here', class: 'with-character-counter' %>
|
||||
<small class="help-text">Your Notebook.ai profile will be available at https://www.notebook.ai/@username.</small><br />
|
||||
<small class="help-text">Up to 40 numbers, letters, and/or the following symbols are allowed: <strong>- _ $ + ! *</strong></small>
|
||||
<div class="space-y-8">
|
||||
<!-- Public information section -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Public information</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">This information is visible to other users on <%= link_to 'your profile', current_user, class: "text-notebook-blue hover:text-blue-700" %>.</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<!-- Name and username -->
|
||||
<div>
|
||||
<label for="user_name" class="block text-sm font-medium text-gray-700">Name (visible on your profile and forum posts)</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="row">
|
||||
<div class="col s12 m6 l4">
|
||||
<label>Your current avatar</label>
|
||||
<br /><br />
|
||||
<%= image_tag current_user.image_url(size=160), class: 'hoverable materialboxed' %>
|
||||
|
||||
<div>
|
||||
<label for="user_username" class="block text-sm font-medium text-gray-700">Username</label>
|
||||
<div class="mt-1 relative rounded-md shadow-sm">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<span class="text-gray-500 sm:text-sm">@</span>
|
||||
</div>
|
||||
<div class="col s12 m6 l8">
|
||||
<label>Upload a new avatar</label>
|
||||
<div class="file-field input-field">
|
||||
<div class="blue btn">
|
||||
<span>Upload</span>
|
||||
<%= f.file_field :avatar %>
|
||||
</div>
|
||||
<div class="file-path-wrapper">
|
||||
<input class="file-path validate" type="text" placeholder="Upload an image">
|
||||
</div>
|
||||
</div>
|
||||
<div class="help-text">
|
||||
Supported file types: .png, .jpg, .jpeg, .gif
|
||||
</div>
|
||||
<div class="help-text">
|
||||
After selecting an image here, submit your changes with the usual "Save changes" button at the bottom of this page to save it.
|
||||
</div>
|
||||
<%= 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" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">Your Notebook.ai profile will be available at https://www.notebook.ai/@username.</p>
|
||||
<p class="mt-1 text-sm text-gray-500">Up to 40 numbers, letters, and/or the following symbols are allowed: <span class="font-semibold">- _ $ + ! *</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avatar section -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Profile picture</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Your avatar is displayed on your profile and next to your posts</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="sm:flex sm:items-start sm:justify-between">
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="mb-4 sm:mb-0 sm:mr-6 flex-shrink-0">
|
||||
<%= image_tag current_user.image_url(size=160), class: 'h-24 w-24 rounded-full object-cover shadow-sm' %>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-900">Upload a new avatar</h4>
|
||||
<p class="text-sm text-gray-500 mt-1">Supported file types: .png, .jpg, .jpeg, .gif</p>
|
||||
<p class="text-sm text-gray-500 mt-1">After selecting an image, submit your changes with the "Save changes" button to save it.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-0">
|
||||
<div class="flex items-center">
|
||||
<label for="user_avatar" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue cursor-pointer">
|
||||
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
Upload
|
||||
<%= f.file_field :avatar, class: "hidden" %>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="input-field">
|
||||
<%= f.text_area :bio, class: 'materialize-textarea with-character-counter', data: { length: 500 } %>
|
||||
<%= f.label 'Bio' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :interests, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Interests' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :favorite_genre, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Favorite genre' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :favorite_author, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Favorite author' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :favorite_book, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Favorite book' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :favorite_quote, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Favorite quote' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :inspirations, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Inspirations' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :other_names, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Other names' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :website, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Website' %><br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card lighten-5">
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<%= f.label 'Favorite page type' %><br />
|
||||
<%= f.select(:favorite_page_type, Rails.application.config.content_types[:all].map { |type| [type.name.pluralize, type.name] }, include_blank: 'None') %>
|
||||
<small class="help-text">Selecting a page here will prominently display it on your profile and somewhat theme your profile page to match.</small><br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card yellow lighten-5">
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<%= f.label 'Custom Premium badge text' %><br />
|
||||
<%= f.text_field :forums_badge_text, data: { length: 20 }, class: 'with-character-counter black-text', placeholder: 'Premium Supporter' %>
|
||||
<small class="help-text">Premium users can customize the badge that appears under their name on their profile and next to their name on the forums.</small><br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col s12 m4 grey-text">
|
||||
<h4>Private information</h4>
|
||||
<p class="flow-text">
|
||||
This information is only ever visible to you.
|
||||
</p>
|
||||
<p>
|
||||
Notebook.ai will never share this information, but may also look at it to better understand its users.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="col s12 m8">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<%= f.label 'Email (always completely private)' %><br />
|
||||
<%= f.email_field :email %>
|
||||
|
||||
<!-- Bio and interests -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Bio & interests</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Tell others about yourself and your writing</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div>
|
||||
<label for="user_bio" class="block text-sm font-medium text-gray-700">Bio</label>
|
||||
<div class="mt-1">
|
||||
<%= 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 } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="input-field">
|
||||
<%= f.text_field :age, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Age' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :location, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Location' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :gender, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Gender' %><br />
|
||||
</div>
|
||||
|
||||
<div class="input-field">
|
||||
<%= f.text_field :occupation, class: 'materialize-textarea' %>
|
||||
<%= f.label 'Occupation' %><br />
|
||||
|
||||
<div>
|
||||
<label for="user_interests" class="block text-sm font-medium text-gray-700">Interests</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
Last login
|
||||
<%= time_ago_in_words current_user.current_sign_in_at %> ago
|
||||
from IP
|
||||
<span class="spoiler"><%= current_user.current_sign_in_ip %></span>.
|
||||
|
||||
<div>
|
||||
<label for="user_favorite_genre" class="block text-sm font-medium text-gray-700">Favorite genre</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_favorite_author" class="block text-sm font-medium text-gray-700">Favorite author</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_favorite_book" class="block text-sm font-medium text-gray-700">Favorite book</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_favorite_quote" class="block text-sm font-medium text-gray-700">Favorite quote</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_inspirations" class="block text-sm font-medium text-gray-700">Inspirations</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_other_names" class="block text-sm font-medium text-gray-700">Other names</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_website" class="block text-sm font-medium text-gray-700">Website</label>
|
||||
<div class="mt-1 relative rounded-md shadow-sm">
|
||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
</div>
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
||||
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
|
||||
<% end %>
|
||||
|
||||
<div class="row">
|
||||
<div class="col s12 m4 grey-text">
|
||||
<h4>Password change</h4>
|
||||
<p class="flow-text">
|
||||
Leave this blank unless you want to change your password.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col s12 m8">
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<div class="field">
|
||||
<%= f.label 'New password' %><br />
|
||||
<%= f.password_field :password, autocomplete: "off" %>
|
||||
|
||||
<!-- Favorite page type -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Profile customization</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Customize how your profile appears to others</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div>
|
||||
<label for="user_favorite_page_type" class="block text-sm font-medium text-gray-700">Favorite page type</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" }) %>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<%= f.label 'New password (again)' %><br />
|
||||
<%= f.password_field :password_confirmation, autocomplete: "off" %>
|
||||
<p class="mt-2 text-sm text-gray-500">Selecting a page here will prominently display it on your profile and somewhat theme your profile page to match.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_forums_badge_text" class="block text-sm font-medium text-gray-700">Custom Premium badge text</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-gray-500">Premium users can customize the badge that appears under their name on their profile and next to their name on the forums.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Private information section -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Private information</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">This information is only ever visible to you</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div>
|
||||
<label for="user_email" class="block text-sm font-medium text-gray-700">Email (always completely private)</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_age" class="block text-sm font-medium text-gray-700">Age</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_location" class="block text-sm font-medium text-gray-700">Location</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_gender" class="block text-sm font-medium text-gray-700">Gender</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_occupation" class="block text-sm font-medium text-gray-700">Occupation</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-gray-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm text-gray-700">
|
||||
Last login <span class="font-medium"><%= time_ago_in_words current_user.current_sign_in_at %> ago</span>
|
||||
from IP
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 cursor-pointer hover:bg-gray-200" onclick="this.classList.toggle('bg-gray-100')"><%= current_user.current_sign_in_ip %></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
|
||||
<div class="rounded-md bg-blue-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<p class="text-sm text-blue-700">
|
||||
Currently waiting confirmation for: <span class="font-medium"><%= resource.unconfirmed_email %></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Password change section -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Password change</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Leave this blank unless you want to change your password</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div>
|
||||
<label for="user_password" class="block text-sm font-medium text-gray-700">New password</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="user_password_confirmation" class="block text-sm font-medium text-gray-700">New password (again)</label>
|
||||
<div class="mt-1">
|
||||
<%= 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" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,58 +1,152 @@
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<h4>To delete your account</h4>
|
||||
<p>
|
||||
This action is <span class="red-text">IRREVERSIBLE</span>. Your account and all of your content
|
||||
will be immediately and permanently deleted. We will miss you!
|
||||
</p>
|
||||
<p>
|
||||
<br />
|
||||
<a class="waves-effect waves-light btn red modal-trigger" href="#modal1">Delete my account</a>
|
||||
|
||||
<div id="modal1" class="modal modal-fixed-footer">
|
||||
<div class="modal-content">
|
||||
<h4>Delete my account</h4>
|
||||
<p>
|
||||
I, <%= current_user.email %>, want to delete my account in full. I understand that my entire account,
|
||||
as well as any and all content created by me, including any content I've contributed to other
|
||||
universes but still own, will be immediately and permanently deleted and will not be recoverable.
|
||||
<div class="space-y-8">
|
||||
<!-- Account deletion card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-red-50 border-b border-red-200">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-base font-medium text-red-800">Delete your account</h3>
|
||||
<p class="text-sm text-red-700">This action is permanent and cannot be undone</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="sm:flex sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">
|
||||
Deleting your account will permanently remove all of your content, including any universes, characters, locations, and other worldbuilding content you've created. This action is <span class="font-bold text-red-600">IRREVERSIBLE</span>.
|
||||
</p>
|
||||
<p>
|
||||
I understand all of my posts on <%= link_to 'the forums', thredded_path %> will also be deleted.
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
<div class="row">
|
||||
<div class="col s12 m6">
|
||||
<h5>Do you need support instead?</h5>
|
||||
<p>
|
||||
You can <a href="https://docs.google.com/forms/d/e/1FAIpQLSe0jnqJlcPJDqwogGere5j8-8F1nSGGYkzbsI-XkOeMnGwLrA/viewform">
|
||||
report a problem</a> (available under the question-mark menu on any page) or reach out
|
||||
directly to an<span>dre</span>w<span></span>@<em></em>indent<span>labs</span>.com. I would be happy to help!
|
||||
</p>
|
||||
<div class="mt-5 space-y-5">
|
||||
<div class="rounded-md bg-yellow-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800">Do you need support instead?</h3>
|
||||
<div class="mt-2 text-sm text-yellow-700">
|
||||
<p>
|
||||
You can <a href="https://docs.google.com/forms/d/e/1FAIpQLSe0jnqJlcPJDqwogGere5j8-8F1nSGGYkzbsI-XkOeMnGwLrA/viewform" class="font-medium underline">report a problem</a> (available under the question-mark menu on any page) or reach out directly to<span>an</span>drew<span></span>@<em></em>indent<span>labs</span>.com. I would be happy to help!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col s12 m6">
|
||||
<h5>
|
||||
Do you want to leave feedback?
|
||||
</h5>
|
||||
<p>
|
||||
We listen to any and all feedback, no matter how small or large.
|
||||
|
||||
<div class="rounded-md bg-blue-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-blue-800">Do you want to leave feedback?</h3>
|
||||
<div class="mt-2 text-sm text-blue-700">
|
||||
<p>We listen to any and all feedback, no matter how small or large.</p>
|
||||
<div class="mt-3">
|
||||
<%= link_to 'Leave feedback', 'https://docs.google.com/forms/d/e/1FAIpQLScZWEVMgm8hBWIIVj1LPzo0GqflmWUrLQlc4TAYqsaS087oAA/viewform', class: 'inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 sm:mt-0 sm:ml-6 sm:flex-shrink-0 sm:flex sm:items-center">
|
||||
<button type="button" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" data-modal-target="delete-account-modal">
|
||||
Delete account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Account Modal -->
|
||||
<div id="delete-account-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<!-- Background overlay -->
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
|
||||
|
||||
<!-- Modal panel -->
|
||||
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||
<div class="sm:flex sm:items-start">
|
||||
<div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<!-- Heroicon name: outline/exclamation -->
|
||||
<svg class="h-6 w-6 text-red-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
|
||||
Delete my account
|
||||
</h3>
|
||||
<div class="mt-2">
|
||||
<p class="text-sm text-gray-500">
|
||||
I, <%= current_user.email %>, want to delete my account in full. I understand that my entire account,
|
||||
as well as any and all content created by me, including any content I've contributed to other
|
||||
universes but still own, will be immediately and permanently deleted and will not be recoverable.
|
||||
</p>
|
||||
<p>
|
||||
<br /><br />
|
||||
<%= link_to 'Leave feedback', 'https://docs.google.com/forms/d/e/1FAIpQLScZWEVMgm8hBWIIVj1LPzo0GqflmWUrLQlc4TAYqsaS087oAA/viewform', class: 'btn' %>
|
||||
<p class="text-sm text-gray-500 mt-2">
|
||||
I understand all of my posts on <%= link_to 'the forums', thredded_path, class: "text-notebook-blue hover:text-blue-700" %> will also be deleted.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<%= link_to delete_my_account_path, class: 'modal-action modal-close waves-effect waves-green btn-flat', method: :DELETE do %>
|
||||
Delete my account
|
||||
<% end %>
|
||||
|
||||
<a href="#!" class="modal-close waves-effect waves-green btn-flat">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
||||
<%= link_to delete_my_account_path, class: 'w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm', method: :DELETE do %>
|
||||
Delete my account
|
||||
<% end %>
|
||||
<button type="button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" data-modal-close="delete-account-modal">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Modal functionality
|
||||
const modalTriggers = document.querySelectorAll('[data-modal-target]');
|
||||
const modalCloseButtons = document.querySelectorAll('[data-modal-close]');
|
||||
|
||||
modalTriggers.forEach(trigger => {
|
||||
trigger.addEventListener('click', function() {
|
||||
const modal = document.getElementById(this.dataset.modalTarget);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
modalCloseButtons.forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const modal = document.getElementById(this.dataset.modalClose);
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.addEventListener('click', function(event) {
|
||||
const modals = document.querySelectorAll('[id$="-modal"]');
|
||||
modals.forEach(modal => {
|
||||
if (event.target === modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,95 +1,151 @@
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<h4>Your Notebook.ai design</h4>
|
||||
<div class="field">
|
||||
<%= f.label :fluid_preference do %>
|
||||
<%= f.check_box :fluid_preference %>
|
||||
<span class="black-text">
|
||||
I want to use <strong>full-width</strong> Notebook.ai.
|
||||
<div class="help-text">Great for small monitors/laptops, phones, and tablets.</div>
|
||||
</span>
|
||||
<% end %>
|
||||
<div class="space-y-8">
|
||||
<!-- Design preferences card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Your Notebook.ai design</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Customize how Notebook.ai looks and feels</p>
|
||||
</div>
|
||||
<br />
|
||||
<div class="field">
|
||||
<%= f.label :dark_mode_enabled do %>
|
||||
<%= f.check_box :dark_mode_enabled %>
|
||||
<span class="black-text">
|
||||
I want to use <strong>dark mode</strong>.
|
||||
<div class="help-text">Great for night-time worldbuilding sessions.</div>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="user_fluid_preference" class="block text-sm font-medium text-gray-700">Full-width layout</label>
|
||||
<p class="text-sm text-gray-500">Great for small monitors/laptops, phones, and tablets.</p>
|
||||
</div>
|
||||
<div class="toggle-switch relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue bg-gray-200" role="switch" aria-checked="false" tabindex="0">
|
||||
<%= f.check_box :fluid_preference, class: "sr-only" %>
|
||||
<span aria-hidden="true" class="toggle-dot pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="user_dark_mode_enabled" class="block text-sm font-medium text-gray-700">Dark mode</label>
|
||||
<p class="text-sm text-gray-500">Great for night-time worldbuilding sessions.</p>
|
||||
</div>
|
||||
<div class="toggle-switch relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue bg-gray-200" role="switch" aria-checked="false" tabindex="0">
|
||||
<%= f.check_box :dark_mode_enabled, class: "sr-only" %>
|
||||
<span aria-hidden="true" class="toggle-dot pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<button type="button" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue" data-tooltip="Preview how your settings will look">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
Preview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<h4>Accessibility settings</h4>
|
||||
<div class="field">
|
||||
<%= f.label :keyboard_shortcuts_preference do %>
|
||||
<%= f.check_box :keyboard_shortcuts_preference %>
|
||||
<span class="black-text">
|
||||
Enable keyboard shortcuts that let me navigate around the site without using a mouse.<br />
|
||||
<div class="help-text">
|
||||
Press <strong>?</strong> to view what's available when the feature is enabled.
|
||||
<!-- Accessibility settings card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Accessibility settings</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Make Notebook.ai work better for you</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="user_keyboard_shortcuts_preference" class="block text-sm font-medium text-gray-700">Keyboard shortcuts</label>
|
||||
<p class="text-sm text-gray-500">Enable navigation around the site without using a mouse. Press <span class="font-semibold">?</span> to view available shortcuts.</p>
|
||||
</div>
|
||||
<div class="toggle-switch relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue bg-gray-200" role="switch" aria-checked="false" tabindex="0">
|
||||
<%= f.check_box :keyboard_shortcuts_preference, class: "sr-only" %>
|
||||
<span aria-hidden="true" class="toggle-dot pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification preferences card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Notification preferences</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Control how and when you receive updates</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="user_notification_updates" class="block text-sm font-medium text-gray-700">In-app notifications</label>
|
||||
<p class="text-sm text-gray-500">Receive occasional notifications inside Notebook.ai about new features.</p>
|
||||
</div>
|
||||
<div class="toggle-switch relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue bg-gray-200" role="switch" aria-checked="false" tabindex="0">
|
||||
<%= f.check_box :notification_updates, class: "sr-only" %>
|
||||
<span aria-hidden="true" class="toggle-dot pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="user_email_updates" class="block text-sm font-medium text-gray-700">Email updates</label>
|
||||
<p class="text-sm text-gray-500">Receive very rare updates by email about new Notebook.ai features.</p>
|
||||
</div>
|
||||
<div class="toggle-switch relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue bg-gray-200" role="switch" aria-checked="false" tabindex="0">
|
||||
<%= f.check_box :email_updates, class: "sr-only" %>
|
||||
<span aria-hidden="true" class="toggle-dot pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-blue-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-blue-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<h4>Notification preferences</h4>
|
||||
<div class="field">
|
||||
<%= f.label :notification_updates do %>
|
||||
<%= f.check_box :notification_updates %>
|
||||
<span class="black-text">I want to receive <strong>occasional notifications</strong> inside Notebook.ai about new features.</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<br />
|
||||
<div class="field">
|
||||
<%= f.label :email_updates do %>
|
||||
<%= f.check_box :email_updates %>
|
||||
<span class="black-text">I want to receive <strong>very rare updates by email</strong> about new Notebook.ai features.</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<br />
|
||||
<div class="field">
|
||||
To change your forum preferences, including email notifications for posts you are following, please
|
||||
<%= link_to thredded.edit_global_preferences_path do %>
|
||||
click here.
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-content">
|
||||
<h4>Community preferences</h4>
|
||||
<div class="field">
|
||||
<%= f.label :community_features_enabled do %>
|
||||
<%= f.check_box :community_features_enabled %>
|
||||
<span class="black-text">
|
||||
I want to be able to see and use Notebook.ai community features.
|
||||
<div class="help-text">Unchecking this box will remove all links in the "Community" section of the sidebar.</div>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<br />
|
||||
<div class="field">
|
||||
<%= f.label :private_profile do %>
|
||||
<%= f.check_box :private_profile %>
|
||||
<span class="black-text">
|
||||
Make my profile page private.
|
||||
<div class="help-text">
|
||||
All of your worldbuilding pages, documents, timelines, and other content on Notebook.ai are private by default.
|
||||
Checking this box will also make your <%= link_to 'profile page', current_user %> private.
|
||||
<div class="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p class="text-sm text-blue-700">
|
||||
To change your forum preferences, including email notifications for posts you are following
|
||||
</p>
|
||||
<p class="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
<%= link_to thredded.edit_global_preferences_path, class: "whitespace-nowrap font-medium text-blue-700 hover:text-blue-600" do %>
|
||||
Click here <span aria-hidden="true">→</span>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Community preferences card -->
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 bg-gray-50 border-b border-gray-200">
|
||||
<h3 class="text-base font-medium text-gray-900">Community preferences</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Control your visibility and interaction with the community</p>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-5 sm:p-6 space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="user_community_features_enabled" class="block text-sm font-medium text-gray-700">Community features</label>
|
||||
<p class="text-sm text-gray-500">Enable access to Notebook.ai community features. Unchecking will remove all links in the "Community" section of the sidebar.</p>
|
||||
</div>
|
||||
<div class="toggle-switch relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue bg-gray-200" role="switch" aria-checked="false" tabindex="0">
|
||||
<%= f.check_box :community_features_enabled, class: "sr-only" %>
|
||||
<span aria-hidden="true" class="toggle-dot pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<label for="user_private_profile" class="block text-sm font-medium text-gray-700">Private profile</label>
|
||||
<p class="text-sm text-gray-500">
|
||||
Make your profile page private. All of your worldbuilding pages, documents, timelines, and other content on Notebook.ai are private by default.
|
||||
Checking this will also make your <%= link_to 'profile page', current_user, class: "text-notebook-blue hover:text-blue-700" %> private.
|
||||
</p>
|
||||
</div>
|
||||
<div class="toggle-switch relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue bg-gray-200" role="switch" aria-checked="false" tabindex="0">
|
||||
<%= f.check_box :private_profile, class: "sr-only" %>
|
||||
<span aria-hidden="true" class="toggle-dot pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,25 +1,103 @@
|
||||
<%= form_for(current_user, as: :user, url: registration_path(:user), html: { method: :put }) do |f| %>
|
||||
<% if current_user.errors.any? %>
|
||||
<div class="card red lighten-3">
|
||||
<div class="card-content">
|
||||
<div class="card-title">Please fix the following errors:</div>
|
||||
<ul class="browser-default">
|
||||
<%= devise_error_messages! %>
|
||||
</ul>
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="bg-white shadow-sm sm:rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-900"><%= @page_title || "Account Settings" %></h1>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage your account settings and preferences</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="row">
|
||||
<div id="preferences" class="col s12">
|
||||
<%= render partial: 'devise/registrations/panes/preferences', locals: { f: f } %>
|
||||
|
||||
<div class="flex flex-col lg:flex-row">
|
||||
<!-- Desktop sidebar -->
|
||||
<div class="hidden lg:block lg:w-64 lg:border-r lg:border-gray-200">
|
||||
<%= render 'devise/registrations/settings_sidebar' %>
|
||||
</div>
|
||||
|
||||
<!-- Mobile sidebar toggle -->
|
||||
<div class="lg:hidden px-4 py-3 border-b border-gray-200">
|
||||
<button id="mobile-nav-toggle" type="button" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-notebook-blue">
|
||||
<svg class="-ml-1 mr-2 h-5 w-5 text-gray-500" xmlns="http://www.w3.org/2000/svg" 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>
|
||||
Navigation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile sidebar (hidden by default) -->
|
||||
<div class="settings-sidebar-mobile fixed inset-y-0 left-0 z-40 w-64 bg-white shadow-lg transform -translate-x-full lg:hidden transition-transform duration-300 ease-in-out pt-16">
|
||||
<%= render 'devise/registrations/settings_sidebar' %>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex-1 p-4 sm:p-6">
|
||||
<%= form_for(current_user, as: :user, url: registration_path(:user), html: { method: :put, id: "settings-form" }) do |f| %>
|
||||
<% if current_user.errors.any? %>
|
||||
<div class="mb-6">
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Please fix the following errors:</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<%= devise_error_messages! %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="space-y-8">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium text-gray-900">Preferences</h2>
|
||||
<p class="mt-1 text-sm text-gray-500">Customize your Notebook.ai experience to match your workflow.</p>
|
||||
</div>
|
||||
|
||||
<%= render partial: 'devise/registrations/panes/preferences', locals: { f: f } %>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 border-t border-gray-200 pt-6 flex justify-end">
|
||||
<div class="flex space-x-3">
|
||||
<%= 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' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<div class="actions">
|
||||
<%= f.submit "Save changes", class: 'btn blue' %>
|
||||
<%= link_to "Cancel", :back, class: 'btn grey' %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Mobile navigation
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// Close mobile sidebar when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
if (settingsSidebar &&
|
||||
!settingsSidebar.contains(event.target) &&
|
||||
!mobileNavToggle.contains(event.target) &&
|
||||
settingsSidebar.classList.contains('translate-x-0')) {
|
||||
settingsSidebar.classList.remove('translate-x-0');
|
||||
settingsSidebar.classList.add('-translate-x-full');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user