Dynamic search
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user