Dynamic search

This commit is contained in:
James Pattinson
2025-10-13 20:05:08 +00:00
parent 8fd951fd1f
commit b34ea2ed84
3 changed files with 314 additions and 0 deletions

View File

@@ -168,6 +168,26 @@
</div> </div>
</div> </div>
<!-- Member Search -->
<div class="search-section">
<div class="search-container">
<div class="search-input-wrapper">
<i class="fas fa-search search-icon"></i>
<input type="text"
id="memberSearchInput"
class="search-input"
placeholder="Search members by name or email..."
autocomplete="off">
<button class="search-clear" id="memberSearchClear" style="display: none;">
<i class="fas fa-times"></i>
</button>
</div>
<div class="search-results-info" id="memberSearchInfo" style="display: none;">
<span id="memberSearchCount">0</span> members found
</div>
</div>
</div>
<div class="data-table"> <div class="data-table">
<table class="table" id="membersTable"> <table class="table" id="membersTable">
<thead> <thead>

View File

@@ -683,6 +683,113 @@ body {
color: var(--info-color); color: var(--info-color);
} }
/* Search Section */
.search-section {
margin-bottom: var(--space-6);
}
.search-container {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
max-width: 400px;
}
.search-input {
width: 100%;
padding: var(--space-3) var(--space-4) var(--space-3) var(--space-10);
border: 1px solid var(--gray-300);
border-radius: var(--radius);
font-size: var(--font-size-sm);
transition: var(--transition);
background: var(--white);
}
.search-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgb(37 99 235 / 0.1);
}
.search-input:not(:placeholder-shown) {
padding-right: var(--space-10);
}
.search-icon {
position: absolute;
left: var(--space-3);
color: var(--gray-400);
font-size: var(--font-size-sm);
pointer-events: none;
z-index: 1;
}
.search-clear {
position: absolute;
right: var(--space-3);
background: transparent;
border: none;
color: var(--gray-400);
cursor: pointer;
padding: var(--space-1);
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
transition: var(--transition);
}
.search-clear:hover {
color: var(--gray-600);
background: var(--gray-100);
}
.search-results-info {
font-size: var(--font-size-sm);
color: var(--gray-600);
font-weight: 500;
}
.search-results-info #memberSearchCount {
color: var(--primary-color);
font-weight: 600;
}
/* No results message */
.no-results {
text-align: center;
padding: var(--space-8) var(--space-4);
color: var(--gray-500);
}
.no-results i {
font-size: var(--font-size-3xl);
color: var(--gray-300);
margin-bottom: var(--space-4);
}
.no-results h3 {
font-size: var(--font-size-lg);
font-weight: 600;
margin-bottom: var(--space-2);
color: var(--gray-700);
}
.no-results p {
font-size: var(--font-size-sm);
margin-bottom: var(--space-4);
}
.no-results .btn {
margin-top: var(--space-2);
}
/* Notifications */ /* Notifications */
.notification { .notification {
display: flex; display: flex;
@@ -1153,6 +1260,18 @@ body {
gap: var(--space-4); gap: var(--space-4);
align-items: stretch; align-items: stretch;
} }
.search-input-wrapper {
max-width: none;
}
.search-input {
font-size: 16px; /* Prevent zoom on iOS */
}
.search-container {
margin-bottom: var(--space-4);
}
.table-responsive { .table-responsive {
overflow-x: auto; overflow-x: auto;

View File

@@ -97,6 +97,38 @@ class MailingListApp {
// Switch to users tab // Switch to users tab
this.switchToUsersTab(); this.switchToUsersTab();
}); });
// Member search functionality
const memberSearchInput = document.getElementById('memberSearchInput');
const memberSearchClear = document.getElementById('memberSearchClear');
memberSearchInput.addEventListener('input', (e) => {
this.filterMembers(e.target.value);
// Show/hide clear button
if (e.target.value.trim()) {
memberSearchClear.style.display = 'flex';
} else {
memberSearchClear.style.display = 'none';
}
});
memberSearchClear.addEventListener('click', () => {
memberSearchInput.value = '';
memberSearchClear.style.display = 'none';
this.filterMembers('');
memberSearchInput.focus();
});
// Clear search when switching tabs
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.tab !== 'members') {
memberSearchInput.value = '';
memberSearchClear.style.display = 'none';
}
});
});
} }
/** /**
@@ -728,6 +760,149 @@ class MailingListApp {
uiManager.setLoading(false); uiManager.setLoading(false);
} }
} }
/**
* Filter members by search term (name or email)
*/
filterMembers(searchTerm) {
const searchInfo = document.getElementById('memberSearchInfo');
const searchCount = document.getElementById('memberSearchCount');
const tbody = document.getElementById('membersTableBody');
if (!this.members || !Array.isArray(this.members)) {
return;
}
// If no search term, show all members
if (!searchTerm || searchTerm.trim() === '') {
this.renderMembers();
searchInfo.style.display = 'none';
return;
}
const normalizedSearch = searchTerm.trim().toLowerCase();
// Filter members by name or email
const filteredMembers = this.members.filter(member => {
const name = (member.name || '').toLowerCase();
const email = (member.email || '').toLowerCase();
return name.includes(normalizedSearch) || email.includes(normalizedSearch);
});
// Update search results info
searchCount.textContent = filteredMembers.length;
searchInfo.style.display = 'block';
// Render filtered results
this.renderFilteredMembers(filteredMembers);
}
/**
* Render filtered members (similar to renderMembers but with filtered data)
*/
renderFilteredMembers(filteredMembers) {
const tbody = document.getElementById('membersTableBody');
tbody.innerHTML = '';
if (filteredMembers.length === 0) {
// Show no results message
const row = tbody.insertRow();
const cell = row.insertCell();
cell.colSpan = 5;
cell.className = 'no-results';
cell.innerHTML = `
<i class="fas fa-search"></i>
<h3>No members found</h3>
<p>Try adjusting your search terms or check the spelling.</p>
`;
return;
}
filteredMembers.forEach(member => {
const row = tbody.insertRow();
// Name
const nameCell = row.insertCell();
nameCell.textContent = member.name;
// Email
const emailCell = row.insertCell();
const emailLink = document.createElement('a');
emailLink.href = `mailto:${member.email}`;
emailLink.textContent = member.email;
emailLink.style.color = 'var(--primary-color)';
emailCell.appendChild(emailLink);
// Lists (show member's subscriptions)
const listsCell = row.insertCell();
const memberLists = [];
this.subscriptions.forEach((members, listId) => {
if (members.some(m => m.member_id === member.member_id)) {
const list = this.lists.find(l => l.list_id === listId);
if (list) {
memberLists.push(list.list_name);
}
}
});
listsCell.textContent = memberLists.length > 0 ? memberLists.join(', ') : 'None';
// Status
const statusCell = row.insertCell();
const statusBadge = document.createElement('span');
statusBadge.className = `status-badge ${member.active ? 'active' : 'inactive'}`;
statusBadge.innerHTML = `<i class="fas fa-${member.active ? 'check' : 'times'}"></i> ${member.active ? 'Active' : 'Inactive'}`;
// Add bounce status if exists
if (member.bounce_status && member.bounce_status !== 'clean') {
const bounceIndicator = document.createElement('span');
bounceIndicator.className = `bounce-badge bounce-${member.bounce_status === 'hard_bounce' ? 'hard' : 'soft'}`;
bounceIndicator.innerHTML = `<i class="fas fa-exclamation-triangle"></i> Bounces`;
bounceIndicator.title = `${member.bounce_count || 0} bounces - ${member.bounce_status}`;
statusCell.appendChild(document.createElement('br'));
statusCell.appendChild(bounceIndicator);
}
statusCell.appendChild(statusBadge);
// Actions
const actionsCell = row.insertCell();
actionsCell.className = 'action-buttons';
// Bounces button (if member has bounce data)
if (member.bounce_count > 0) {
const bouncesBtn = uiManager.createActionButton('Bounces', 'exclamation-triangle', 'btn-warning', () => {
uiManager.showBounceHistory(member);
});
actionsCell.appendChild(bouncesBtn);
}
const subscriptionsBtn = uiManager.createActionButton('Lists', 'list', 'btn-secondary', () => {
uiManager.showMemberSubscriptions(member);
});
actionsCell.appendChild(subscriptionsBtn);
const editBtn = uiManager.createActionButton('Edit', 'edit', 'btn-primary', () => {
uiManager.showMemberModal(member);
});
editBtn.setAttribute('data-requires-write', '');
actionsCell.appendChild(editBtn);
const deleteBtn = uiManager.createActionButton('Delete', 'trash', 'btn-danger', () => {
uiManager.showConfirmation(
`Are you sure you want to delete member "${member.name}"? This will also remove them from all mailing lists.`,
async () => {
await this.deleteMember(member.member_id);
}
);
});
deleteBtn.setAttribute('data-requires-write', '');
actionsCell.appendChild(deleteBtn);
});
// Update UI permissions for the filtered results
const hasWriteAccess = this.currentUser && (this.currentUser.role === 'administrator' || this.currentUser.role === 'operator');
this.updateUIForPermissions(hasWriteAccess);
}
} }
// Initialize the application when DOM is loaded // Initialize the application when DOM is loaded