start on account settings redesign

This commit is contained in:
Andrew Brown 2025-06-15 00:36:38 -05:00
parent e02f99d354
commit 4c07948486
11 changed files with 1636 additions and 384 deletions

View File

@ -1,3 +1,4 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css
//= link settings.js

View 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);
}
});
});
}

View File

@ -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
View 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);
}
});
});
}

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&rarr;</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>

View File

@ -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>