Mass UI Updates

This commit is contained in:
2025-12-15 14:54:29 -06:00
parent cf1ffac9ee
commit 13bb6542bc
11 changed files with 3060 additions and 121 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ async function login(email, password) {
*/
function logout() {
pb.authStore.clear();
window.location.href = 'index.html';
window.location.href = 'index';
}
/**
@@ -26,7 +26,7 @@ function logout() {
*/
function checkAuth() {
if (!pb.authStore.isValid) {
window.location.href = 'index.html';
window.location.href = 'index';
return false;
}
return true;
+197
View File
@@ -0,0 +1,197 @@
// Custom dropdown component with liquid glass styling
// Replaces native select elements with styled custom dropdowns
/**
* Initialize custom dropdowns for all select elements
*/
function initCustomDropdowns() {
const selects = document.querySelectorAll('select');
selects.forEach(select => {
// Skip if already converted
if (select.dataset.customDropdown === 'true') {
return;
}
createCustomDropdown(select);
});
}
/**
* Create a custom dropdown for a select element
*/
function createCustomDropdown(select) {
// Mark as converted
select.dataset.customDropdown = 'true';
// Create dropdown container
const dropdownContainer = document.createElement('div');
dropdownContainer.className = 'relative';
// Create button
const button = document.createElement('button');
button.type = 'button';
button.className = 'w-full px-3 py-2 border-2 border-gray-300/60 rounded-xl backdrop-blur-sm bg-white/40 focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/70 outline-none text-base transition-all shadow-sm text-left flex items-center justify-between';
button.id = select.id + '_button';
// Get selected option text
const selectedOption = select.options[select.selectedIndex];
const buttonText = document.createElement('span');
buttonText.className = 'flex-1 text-gray-900';
buttonText.textContent = selectedOption ? selectedOption.text : 'Select...';
button.appendChild(buttonText);
// Add chevron icon
const chevron = document.createElement('svg');
chevron.className = 'w-5 h-5 text-gray-400 flex-shrink-0 ml-2 transition-transform';
chevron.id = select.id + '_chevron';
chevron.fill = 'none';
chevron.stroke = 'currentColor';
chevron.viewBox = '0 0 24 24';
chevron.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>';
button.appendChild(chevron);
// Create dropdown menu
const menu = document.createElement('div');
menu.className = 'hidden absolute z-50 w-full mt-1 backdrop-blur-xl bg-white/80 rounded-2xl shadow-2xl border border-white/30 overflow-hidden max-h-60 overflow-y-auto';
menu.id = select.id + '_menu';
// Create options
Array.from(select.options).forEach((option, index) => {
const menuItem = document.createElement('button');
menuItem.type = 'button';
menuItem.className = 'w-full text-left px-4 py-2.5 text-sm text-gray-700 hover:bg-white/40 focus:outline-none focus:bg-white/40 transition-colors';
if (option.value === select.value) {
menuItem.className += ' bg-indigo-50/50 text-indigo-600';
}
menuItem.textContent = option.text;
menuItem.dataset.value = option.value;
menuItem.addEventListener('click', () => {
select.value = option.value;
buttonText.textContent = option.text;
select.dispatchEvent(new Event('change', { bubbles: true }));
closeDropdown(select.id);
// Update selected state
menu.querySelectorAll('button').forEach(item => {
if (item.dataset.value === option.value) {
item.className = 'w-full text-left px-4 py-2.5 text-sm text-indigo-600 bg-indigo-50/50 hover:bg-white/40 focus:outline-none focus:bg-white/40 transition-colors';
} else {
item.className = 'w-full text-left px-4 py-2.5 text-sm text-gray-700 hover:bg-white/40 focus:outline-none focus:bg-white/40 transition-colors';
}
});
});
menu.appendChild(menuItem);
});
// Toggle dropdown
button.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = !menu.classList.contains('hidden');
// Close all other dropdowns
document.querySelectorAll('[id$="_menu"]').forEach(m => {
if (m.id !== menu.id) {
m.classList.add('hidden');
const otherChevron = document.getElementById(m.id.replace('_menu', '_chevron'));
if (otherChevron) {
otherChevron.classList.remove('rotate-180');
}
}
});
if (isOpen) {
closeDropdown(select.id);
} else {
openDropdown(select.id);
}
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!dropdownContainer.contains(e.target)) {
closeDropdown(select.id);
}
});
// Wrap select and add custom elements
select.style.display = 'none';
select.style.position = 'absolute';
select.style.opacity = '0';
select.style.pointerEvents = 'none';
dropdownContainer.appendChild(button);
dropdownContainer.appendChild(menu);
select.parentNode.insertBefore(dropdownContainer, select);
dropdownContainer.appendChild(select);
// Listen for programmatic changes to select
select.addEventListener('change', () => {
updateCustomDropdown(select.id);
});
}
/**
* Open dropdown
*/
function openDropdown(selectId) {
const menu = document.getElementById(selectId + '_menu');
const chevron = document.getElementById(selectId + '_chevron');
if (menu) {
menu.classList.remove('hidden');
if (chevron) {
chevron.classList.add('rotate-180');
}
}
}
/**
* Close dropdown
*/
function closeDropdown(selectId) {
const menu = document.getElementById(selectId + '_menu');
const chevron = document.getElementById(selectId + '_chevron');
if (menu) {
menu.classList.add('hidden');
if (chevron) {
chevron.classList.remove('rotate-180');
}
}
}
/**
* Update custom dropdown when select value changes programmatically
*/
function updateCustomDropdown(selectId) {
const select = document.getElementById(selectId);
if (!select || select.dataset.customDropdown !== 'true') {
return;
}
const button = document.getElementById(selectId + '_button');
const buttonText = button?.querySelector('span');
const selectedOption = select.options[select.selectedIndex];
if (buttonText && selectedOption) {
buttonText.textContent = selectedOption.text;
}
// Update selected state in menu
const menu = document.getElementById(selectId + '_menu');
if (menu) {
menu.querySelectorAll('button').forEach(item => {
if (item.dataset.value === select.value) {
item.className = 'w-full text-left px-4 py-2.5 text-sm text-indigo-600 bg-indigo-50/50 hover:bg-white/40 focus:outline-none focus:bg-white/40 transition-colors';
} else {
item.className = 'w-full text-left px-4 py-2.5 text-sm text-gray-700 hover:bg-white/40 focus:outline-none focus:bg-white/40 transition-colors';
}
});
}
}
// Make functions globally accessible
if (typeof window !== 'undefined') {
window.initCustomDropdowns = initCustomDropdowns;
window.createCustomDropdown = createCustomDropdown;
window.updateCustomDropdown = updateCustomDropdown;
}
+43 -9
View File
@@ -208,7 +208,7 @@ function displayJobsCards(jobs) {
return `
<div
onclick="openJobModal('${job.id}')"
class="bg-white rounded-lg shadow border border-gray-200 p-6 hover:shadow-lg hover:border-indigo-300 cursor-pointer transition-all duration-200 hover:bg-indigo-50/30"
class="backdrop-blur-xl bg-white/60 rounded-2xl shadow-xl border border-white/30 p-6 hover:shadow-2xl hover:border-white/50 hover:bg-white/70 cursor-pointer transition-all duration-300 hover:scale-[1.02]"
>
<div class="mb-4">
<div class="flex items-center justify-between">
@@ -226,6 +226,7 @@ function displayJobsCards(jobs) {
<div>
<span class="text-xs text-gray-500 uppercase tracking-wide">Client</span>
<p class="text-sm font-medium text-gray-900 mt-1">${job.Company_Client || '-'}</p>
${job.Phone_Number ? `<a href="tel:${job.Phone_Number.replace(/[^\d+]/g, '')}" onclick="event.stopPropagation()" class="text-xs text-indigo-600 hover:text-indigo-800 mt-1 block hover:underline">${job.Phone_Number}</a>` : ''}
</div>
<div>
@@ -242,6 +243,31 @@ function displayJobsCards(jobs) {
</div>
</div>
${job.Job_Address ? `
<div>
<span class="text-xs text-gray-500 uppercase tracking-wide">Address</span>
<p class="text-sm font-medium text-gray-900 mt-1">${job.Job_Address}</p>
<div class="mt-2 flex flex-col gap-2">
<a
href="https://maps.apple.com/?q=${encodeURIComponent(job.Job_Address)}"
target="_blank"
onclick="event.stopPropagation()"
class="w-full px-3 py-1.5 backdrop-blur-sm bg-gray-900/90 text-white text-xs font-medium rounded-xl hover:bg-gray-800/90 transition-all shadow-lg hover:shadow-xl text-center"
>
Apple Maps
</a>
<a
href="https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(job.Job_Address)}"
target="_blank"
onclick="event.stopPropagation()"
class="w-full px-3 py-1.5 backdrop-blur-sm bg-blue-600/90 text-white text-xs font-medium rounded-xl hover:bg-blue-700/90 transition-all shadow-lg hover:shadow-xl text-center"
>
Google Maps
</a>
</div>
</div>
` : ''}
<div class="grid grid-cols-2 gap-4 sm:col-span-2 lg:col-span-1">
<div>
<span class="text-xs text-gray-500 uppercase tracking-wide">Type</span>
@@ -278,26 +304,34 @@ function updateViewDisplay() {
const cardViewBtn = document.getElementById('cardViewBtn');
if (currentView === 'table') {
// Show table, hide card
if (tableView) tableView.classList.remove('hidden');
if (cardView) cardView.classList.add('hidden');
// Table button: active (indigo with shadow)
if (tableViewBtn) {
tableViewBtn.classList.add('bg-indigo-600', 'text-white');
tableViewBtn.classList.remove('text-gray-700', 'hover:bg-gray-50');
tableViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
tableViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
}
// Card button: inactive (transparent)
if (cardViewBtn) {
cardViewBtn.classList.remove('bg-indigo-600', 'text-white');
cardViewBtn.classList.add('text-gray-700', 'hover:bg-gray-50');
cardViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
cardViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
}
} else {
// Show card, hide table
if (tableView) tableView.classList.add('hidden');
if (cardView) cardView.classList.remove('hidden');
// Table button: inactive (transparent)
if (tableViewBtn) {
tableViewBtn.classList.remove('bg-indigo-600', 'text-white');
tableViewBtn.classList.add('text-gray-700', 'hover:bg-gray-50');
tableViewBtn.classList.remove('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
tableViewBtn.classList.add('text-gray-700', 'hover:bg-white/40');
}
// Card button: active (indigo with shadow)
if (cardViewBtn) {
cardViewBtn.classList.add('bg-indigo-600', 'text-white');
cardViewBtn.classList.remove('text-gray-700', 'hover:bg-gray-50');
cardViewBtn.classList.remove('text-gray-700', 'hover:bg-white/40');
cardViewBtn.classList.add('bg-indigo-600/90', 'text-white', 'shadow-lg', 'hover:bg-indigo-700/90');
}
}
}
+267
View File
@@ -0,0 +1,267 @@
// User preferences management
// Stores preferences in the users collection PRISM column as JSON
// Uses "Job-Pages" key to avoid conflicts with other applications
/**
* Get user preferences from PRISM column
*/
async function getUserPreferences() {
try {
const user = pb.authStore.model;
if (!user) {
return getDefaultPreferences();
}
// Get current user record
const userRecord = await pb.collection('users').getOne(user.id);
// Parse PRISM JSON if it exists
let prism = {};
if (userRecord.PRISM) {
try {
prism = typeof userRecord.PRISM === 'string'
? JSON.parse(userRecord.PRISM)
: userRecord.PRISM;
} catch (e) {
console.warn('Error parsing PRISM data:', e);
prism = {};
}
}
// Get Job-Pages preferences (isolated from other apps)
const jobPagesPrefs = prism['Job-Pages'] || {};
// Merge with defaults
return {
darkMode: jobPagesPrefs.darkMode ?? false,
defaultView: jobPagesPrefs.defaultView ?? 'table'
};
} catch (error) {
console.error('Error loading preferences:', error);
return getDefaultPreferences();
}
}
/**
* Get default preferences
*/
function getDefaultPreferences() {
return {
darkMode: false,
defaultView: 'table'
};
}
/**
* Save user preferences to PRISM column
*/
async function saveUserPreferences(preferences) {
try {
const user = pb.authStore.model;
if (!user) {
throw new Error('User not authenticated');
}
// Get current user record
const userRecord = await pb.collection('users').getOne(user.id);
// Parse existing PRISM JSON
let prism = {};
if (userRecord.PRISM) {
try {
prism = typeof userRecord.PRISM === 'string'
? JSON.parse(userRecord.PRISM)
: userRecord.PRISM;
} catch (e) {
console.warn('Error parsing PRISM data:', e);
prism = {};
}
}
// Update only Job-Pages preferences (preserve other app data)
prism['Job-Pages'] = {
darkMode: preferences.darkMode,
defaultView: preferences.defaultView
};
// Save back to PRISM column
await pb.collection('users').update(user.id, {
PRISM: JSON.stringify(prism)
});
return true;
} catch (error) {
console.error('Error saving preferences:', error);
throw error;
}
}
/**
* Apply dark mode to the page
*/
function applyDarkMode(enabled) {
const html = document.documentElement;
if (enabled) {
html.classList.add('dark');
localStorage.setItem('darkMode', 'true');
} else {
html.classList.remove('dark');
localStorage.setItem('darkMode', 'false');
}
}
/**
* Initialize dark mode from preferences
*/
async function initializeDarkMode() {
try {
const preferences = await getUserPreferences();
applyDarkMode(preferences.darkMode);
} catch (error) {
console.error('Error initializing dark mode:', error);
}
}
/**
* Update view option visual state
*/
function updateViewOptionVisuals() {
const selectedView = document.querySelector('input[name="defaultView"]:checked')?.value || 'table';
const options = document.querySelectorAll('.view-option');
options.forEach(option => {
const input = option.querySelector('input[type="radio"]');
const icon = option.querySelector('.view-icon');
const title = option.querySelector('.view-title');
const isSelected = input.value === selectedView;
if (isSelected) {
option.classList.add('border-indigo-500', 'bg-indigo-50/50', 'shadow-md');
option.classList.remove('border-white/30', 'bg-white/40');
if (icon) icon.classList.add('text-indigo-600');
if (icon) icon.classList.remove('text-gray-400');
if (title) title.classList.add('text-indigo-600');
if (title) title.classList.remove('text-gray-900');
} else {
option.classList.remove('border-indigo-500', 'bg-indigo-50/50', 'shadow-md');
option.classList.add('border-white/30', 'bg-white/40');
if (icon) icon.classList.remove('text-indigo-600');
if (icon) icon.classList.add('text-gray-400');
if (title) title.classList.remove('text-indigo-600');
if (title) title.classList.add('text-gray-900');
}
});
}
/**
* Load preferences and populate form
*/
async function loadPreferences() {
try {
const preferences = await getUserPreferences();
// Set dark mode toggle
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
darkModeToggle.checked = preferences.darkMode;
applyDarkMode(preferences.darkMode);
}
// Set default view radio buttons
const viewTable = document.getElementById('viewTable');
const viewCard = document.getElementById('viewCard');
if (viewTable && viewCard) {
if (preferences.defaultView === 'table') {
viewTable.checked = true;
} else {
viewCard.checked = true;
}
// Update visual state
updateViewOptionVisuals();
}
// Add change listeners to radio buttons
const radioButtons = document.querySelectorAll('input[name="defaultView"]');
radioButtons.forEach(radio => {
radio.addEventListener('change', updateViewOptionVisuals);
});
} catch (error) {
console.error('Error loading preferences:', error);
}
}
/**
* Save preferences from form
*/
async function savePreferences() {
try {
const darkMode = document.getElementById('darkModeToggle').checked;
const defaultView = document.querySelector('input[name="defaultView"]:checked')?.value || 'table';
const preferences = {
darkMode: darkMode,
defaultView: defaultView
};
await saveUserPreferences(preferences);
applyDarkMode(darkMode);
showMessage('Preferences saved successfully', 'success');
} catch (error) {
console.error('Error saving preferences:', error);
showMessage('Error saving preferences: ' + (error.message || 'Unknown error'), 'error');
}
}
/**
* Show message to user
*/
function showMessage(message, type = 'success') {
const container = document.getElementById('messageContainer');
if (!container) return;
const bgColor = type === 'success'
? 'bg-green-50 border-green-200 text-green-700'
: 'bg-red-50 border-red-200 text-red-700';
container.innerHTML = `
<div class="${bgColor} border px-4 py-3 rounded-xl text-sm backdrop-blur-sm">
${message}
</div>
`;
// Auto-hide after 5 seconds
setTimeout(() => {
container.innerHTML = '';
}, 5000);
}
// Form submission handler
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('preferencesForm');
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
await savePreferences();
});
}
// Dark mode toggle change handler
const darkModeToggle = document.getElementById('darkModeToggle');
if (darkModeToggle) {
darkModeToggle.addEventListener('change', (e) => {
applyDarkMode(e.target.checked);
});
}
});
}
// Make functions globally accessible
if (typeof window !== 'undefined') {
window.getUserPreferences = getUserPreferences;
window.saveUserPreferences = saveUserPreferences;
window.initializeDarkMode = initializeDarkMode;
window.loadPreferences = loadPreferences;
window.savePreferences = savePreferences;
}