Dynamic search
This commit is contained in:
@@ -168,6 +168,26 @@
|
||||
</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">
|
||||
<table class="table" id="membersTable">
|
||||
<thead>
|
||||
|
||||
@@ -683,6 +683,113 @@ body {
|
||||
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 */
|
||||
.notification {
|
||||
display: flex;
|
||||
@@ -1154,6 +1261,18 @@ body {
|
||||
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 {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -97,6 +97,38 @@ class MailingListApp {
|
||||
// Switch to users tab
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
Reference in New Issue
Block a user