Add shared-ip-manager.userscript.js
# Shared IP Manager - Changelog ## Version 1.1.0 - Feature Enhancement Release ### �� New Features #### Advanced Search & Filtering - **Real-time search** across all table columns (username, country, provider, type, etc.) - **Banned user filter toggle** - quickly view only banned users - **Combined filtering** - search within filtered results - **User count display** with banned user statistics #### Column Sorting - **Sortable Username column** - alphabetical A-Z / Z-A - **Sortable Type column** - group similar types together - **Visual sort indicators** - ↕ (unsorted), ↑ (ascending), ↓ (descending) - **Click to toggle** between ascending/descending order #### Banned User Detection & Styling - **Automatic detection** of banned users from data structure - **Visual indicators**: - 🚫 Ban icon before username - Red strikethrough text - "BANNED" badge with red styling - **Maintains functionality** - banned user links still work #### Enhanced UI/UX - **Dark theme styling** matching overlay design - **Responsive table** with horizontal/vertical scrolling - **Hover effects** on sortable headers and buttons - **Professional layout** with proper spacing and typography ### 🔧 Technical Improvements #### Performance - **Efficient DOM manipulation** - direct element creation vs innerHTML - **Optimized filtering** - real-time updates without lag - **Memory management** - proper event listener cleanup #### Code Quality - **Error handling** - null checks and graceful fallbacks - **Debug logging** - comprehensive console output for troubleshooting - **Modular functions** - clean, maintainable code structure ### 🐛 Bug Fixes - **Fixed null reference errors** in event listener setup - **Resolved sorting issues** with proper DOM element access - **Fixed table overflow** - proper scrolling implementation ### 📊 User Interface #### Layout - **Flexbox search bar** with filter button and count display - **Sticky table headers** that remain visible while scrolling - **Consistent spacing** and visual hierarchy #### Visual Design - **Color scheme**: Dark backgrounds (#101113, #131313) with red accents (#dc2626) - **Typography**: System font stack for better readability - **Interactive elements**: Smooth transitions and hover states ### 🎯 Use Cases #### For Moderators - **Quick banned user review** - filter to see only banned users - **Search specific users** - find users by name, country, or provider - **Sort by activity** - organize by username or connection type #### For Analysis - **Data visualization** - clean table format vs overlay - **Export-friendly** - structured data for further analysis - **Real-time filtering** - dynamic data exploration --- ## Version 1.0.0 - Initial Release ### Core Functionality - Transform shared IP overlay into searchable table format - Basic table structure with user data extraction - Initial dark theme implementation --- *This userscript enhances the shared IP management experience by providing powerful filtering, sorting, and visualization tools for better user moderation and data analysis.*
This commit is contained in:
parent
71c588ee01
commit
2733e86c8f
517
shared-ip-manager.userscript.js
Normal file
517
shared-ip-manager.userscript.js
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name Shared IP Manager
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.1.0
|
||||||
|
// @description Transform shared IP overlay into searchable table format
|
||||||
|
// @author Ryahn
|
||||||
|
// @match *://*/*
|
||||||
|
// @grant none
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Function to extract country code from flag class
|
||||||
|
function getCountryCode(flagElement) {
|
||||||
|
const classes = flagElement.className;
|
||||||
|
const match = classes.match(/tck-provider-country-flag\s+(\w+)/);
|
||||||
|
return match ? match[1].toUpperCase() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get country name from flag element
|
||||||
|
function getCountryName(flagElement) {
|
||||||
|
return flagElement.getAttribute('data-original-title') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get provider info
|
||||||
|
function getProviderInfo(providerElement) {
|
||||||
|
return {
|
||||||
|
name: providerElement.textContent.trim(),
|
||||||
|
asn: providerElement.getAttribute('data-original-title') || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get type info
|
||||||
|
function getTypeInfo(typeElement) {
|
||||||
|
return typeElement.getAttribute('data-original-title') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to extract usage count
|
||||||
|
function getUsageCount(usageText) {
|
||||||
|
const match = usageText.match(/(\d+)\s+time/);
|
||||||
|
return match ? parseInt(match[1]) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create flag emoji from country code
|
||||||
|
function getFlagEmoji(countryCode) {
|
||||||
|
if (!countryCode) return '';
|
||||||
|
const codePoints = countryCode
|
||||||
|
.toLowerCase()
|
||||||
|
.split('')
|
||||||
|
.map(char => 127397 + char.charCodeAt());
|
||||||
|
return String.fromCodePoint(...codePoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to create the table
|
||||||
|
function createTable() {
|
||||||
|
const overlay = document.querySelector('.overlay-content');
|
||||||
|
if (!overlay) return;
|
||||||
|
|
||||||
|
// Find all user entries
|
||||||
|
const userEntries = overlay.querySelectorAll('.block-row.block-row--separated');
|
||||||
|
|
||||||
|
if (userEntries.length === 0) return;
|
||||||
|
|
||||||
|
// Create table container
|
||||||
|
const tableContainer = document.createElement('div');
|
||||||
|
tableContainer.style.cssText = `
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create search and filter container
|
||||||
|
const searchFilterContainer = document.createElement('div');
|
||||||
|
searchFilterContainer.style.cssText = `
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create search input
|
||||||
|
const searchInput = document.createElement('input');
|
||||||
|
searchInput.type = 'text';
|
||||||
|
searchInput.placeholder = 'Search users, countries, providers...';
|
||||||
|
searchInput.style.cssText = `
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #101113;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #101113;
|
||||||
|
color: #374151;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create banned filter button
|
||||||
|
const bannedFilterButton = document.createElement('button');
|
||||||
|
bannedFilterButton.textContent = 'Show Banned Only';
|
||||||
|
bannedFilterButton.style.cssText = `
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #dc2626;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #dc262620;
|
||||||
|
color: #dc2626;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add hover effect for button
|
||||||
|
bannedFilterButton.addEventListener('mouseenter', function() {
|
||||||
|
this.style.backgroundColor = '#dc2626';
|
||||||
|
this.style.color = 'white';
|
||||||
|
});
|
||||||
|
bannedFilterButton.addEventListener('mouseleave', function() {
|
||||||
|
if (!this.classList.contains('active')) {
|
||||||
|
this.style.backgroundColor = '#dc262620';
|
||||||
|
this.style.color = '#dc2626';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create count display
|
||||||
|
const countDisplay = document.createElement('div');
|
||||||
|
countDisplay.style.cssText = `
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #17191b;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add button to container
|
||||||
|
searchFilterContainer.appendChild(searchInput);
|
||||||
|
searchFilterContainer.appendChild(bannedFilterButton);
|
||||||
|
searchFilterContainer.appendChild(countDisplay);
|
||||||
|
|
||||||
|
// Create table wrapper for scrolling
|
||||||
|
const tableWrapper = document.createElement('div');
|
||||||
|
tableWrapper.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: calc(100vh - 150px);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
const table = document.createElement('table');
|
||||||
|
table.style.cssText = `
|
||||||
|
width: 100%;
|
||||||
|
min-width: 800px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create table header
|
||||||
|
const thead = document.createElement('thead');
|
||||||
|
thead.style.cssText = `position: sticky; top: 0; z-index: 10;`;
|
||||||
|
|
||||||
|
const headerRow = document.createElement('tr');
|
||||||
|
headerRow.style.cssText = `background: #101113; border-bottom: 1px solid #101113; color: #959595;`;
|
||||||
|
|
||||||
|
// Username header (sortable)
|
||||||
|
const usernameHeader = document.createElement('th');
|
||||||
|
usernameHeader.id = 'sort-username';
|
||||||
|
usernameHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap; cursor: pointer; user-select: none;`;
|
||||||
|
usernameHeader.innerHTML = `Username <span id="username-sort-icon" style="margin-left: 5px;">↕</span>`;
|
||||||
|
|
||||||
|
// Join Date header
|
||||||
|
const joinDateHeader = document.createElement('th');
|
||||||
|
joinDateHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
||||||
|
joinDateHeader.textContent = 'Join Date';
|
||||||
|
|
||||||
|
// Messages header
|
||||||
|
const messagesHeader = document.createElement('th');
|
||||||
|
messagesHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
||||||
|
messagesHeader.textContent = 'Messages';
|
||||||
|
|
||||||
|
// Country header
|
||||||
|
const countryHeader = document.createElement('th');
|
||||||
|
countryHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
||||||
|
countryHeader.textContent = 'Country';
|
||||||
|
|
||||||
|
// Provider header
|
||||||
|
const providerHeader = document.createElement('th');
|
||||||
|
providerHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap;`;
|
||||||
|
providerHeader.textContent = 'Provider';
|
||||||
|
|
||||||
|
// Type header (sortable)
|
||||||
|
const typeHeader = document.createElement('th');
|
||||||
|
typeHeader.id = 'sort-type';
|
||||||
|
typeHeader.style.cssText = `padding: 10px 12px; text-align: left; border-right: 1px solid #17191b; font-weight: 600; color: #959595; white-space: nowrap; cursor: pointer; user-select: none;`;
|
||||||
|
typeHeader.innerHTML = `Type <span id="type-sort-icon" style="margin-left: 5px;">↕</span>`;
|
||||||
|
|
||||||
|
// Usage header
|
||||||
|
const usageHeader = document.createElement('th');
|
||||||
|
usageHeader.style.cssText = `padding: 10px 12px; text-align: left; font-weight: 600; color: #959595; white-space: nowrap;`;
|
||||||
|
usageHeader.textContent = 'Usage';
|
||||||
|
|
||||||
|
// Append all headers to row
|
||||||
|
headerRow.appendChild(usernameHeader);
|
||||||
|
headerRow.appendChild(joinDateHeader);
|
||||||
|
headerRow.appendChild(messagesHeader);
|
||||||
|
headerRow.appendChild(countryHeader);
|
||||||
|
headerRow.appendChild(providerHeader);
|
||||||
|
headerRow.appendChild(typeHeader);
|
||||||
|
headerRow.appendChild(usageHeader);
|
||||||
|
|
||||||
|
thead.appendChild(headerRow);
|
||||||
|
|
||||||
|
// Create table body
|
||||||
|
const tbody = document.createElement('tbody');
|
||||||
|
|
||||||
|
// Process each user entry
|
||||||
|
userEntries.forEach((entry, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.style.cssText = `
|
||||||
|
border-bottom: 1px solid #17191b;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
`;
|
||||||
|
row.style.backgroundColor = index % 2 === 0 ? '#131313' : '#131313';
|
||||||
|
|
||||||
|
// Username
|
||||||
|
const usernameLink = entry.querySelector('h3.contentRow-header a.username');
|
||||||
|
const username = usernameLink ? usernameLink.textContent.trim() : 'N/A';
|
||||||
|
const profileUrl = usernameLink ? usernameLink.href : '#';
|
||||||
|
|
||||||
|
// Check if user is banned
|
||||||
|
const usernameSpan = entry.querySelector('h3.contentRow-header a.username span');
|
||||||
|
const isBanned = usernameSpan && usernameSpan.classList.contains('username--banned');
|
||||||
|
|
||||||
|
// Join date
|
||||||
|
const joinDateElement = entry.querySelector('time.u-dt');
|
||||||
|
const joinDate = joinDateElement ? joinDateElement.textContent.trim() : 'N/A';
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
const dtElements = entry.querySelectorAll('dt');
|
||||||
|
let messagesElement = null;
|
||||||
|
for (const dt of dtElements) {
|
||||||
|
if (dt.textContent.includes('Messages')) {
|
||||||
|
messagesElement = dt;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const messages = messagesElement ?
|
||||||
|
(messagesElement.nextElementSibling ? messagesElement.nextElementSibling.textContent.trim() : '0') : '0';
|
||||||
|
|
||||||
|
// Country flag and name
|
||||||
|
const flagElement = entry.querySelector('.tck-provider-country-flag');
|
||||||
|
const countryCode = flagElement ? getCountryCode(flagElement) : '';
|
||||||
|
const countryName = flagElement ? getCountryName(flagElement) : '';
|
||||||
|
const flagEmoji = getFlagEmoji(countryCode);
|
||||||
|
|
||||||
|
// Provider info
|
||||||
|
const providerElement = entry.querySelector('.tck-provider-txt');
|
||||||
|
const providerInfo = providerElement ? getProviderInfo(providerElement) : { name: 'N/A', asn: '' };
|
||||||
|
|
||||||
|
// Type info
|
||||||
|
const typeElement = entry.querySelector('.tck-provider-type');
|
||||||
|
const typeInfo = typeElement ? getTypeInfo(typeElement) : 'N/A';
|
||||||
|
|
||||||
|
// Usage count
|
||||||
|
const liElements = entry.querySelectorAll('li');
|
||||||
|
let usageElement = null;
|
||||||
|
for (const li of liElements) {
|
||||||
|
if (li.textContent.includes('time')) {
|
||||||
|
usageElement = li;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const usageText = usageElement ? usageElement.textContent.trim() : '0 time';
|
||||||
|
const usageCount = getUsageCount(usageText);
|
||||||
|
|
||||||
|
|
||||||
|
// Build row HTML
|
||||||
|
row.innerHTML = `
|
||||||
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap;">
|
||||||
|
<a href="${profileUrl}" target="_blank" style="color: ${isBanned ? '#dc2626' : '#2563eb'}; text-decoration: ${isBanned ? 'line-through' : 'none'}; font-weight: 500;">
|
||||||
|
${isBanned ? '🚫 ' : ''}${username}
|
||||||
|
</a>
|
||||||
|
${isBanned ? '<span style="color: #dc2626; margin-left: 8px; font-size: 11px; font-weight: 600; background: #dc262620; padding: 2px 6px; border-radius: 3px;">BANNED</span>' : ''}
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap; color: #6b7280;">${joinDate}</td>
|
||||||
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap; color: #6b7280;">${messages}</td>
|
||||||
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap;">
|
||||||
|
<span title="${countryName}" style="color: #374151;">${flagEmoji} ${countryCode}</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap;" title="${providerInfo.asn}">
|
||||||
|
<span style="color: #374151;">${providerInfo.name}</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px 12px; border-right: 1px solid #e5e7eb; white-space: nowrap;" title="${typeInfo}">
|
||||||
|
<span style="color: #6b7280;">${typeInfo.replace('Type: ', '')}</span>
|
||||||
|
</td>
|
||||||
|
<td style="padding: 10px 12px; text-align: center; white-space: nowrap;">
|
||||||
|
<span style="color: #374151; font-weight: 500;">${usageCount}</span>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add hover effect
|
||||||
|
row.addEventListener('mouseenter', () => {
|
||||||
|
row.style.backgroundColor = '#131313';
|
||||||
|
});
|
||||||
|
row.addEventListener('mouseleave', () => {
|
||||||
|
row.style.backgroundColor = index % 2 === 0 ? '#131313' : '#131313';
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter state
|
||||||
|
let showBannedOnly = false;
|
||||||
|
|
||||||
|
// Function to apply both search and filter
|
||||||
|
function applyFilters() {
|
||||||
|
const searchTerm = searchInput.value.toLowerCase();
|
||||||
|
const rows = tbody.querySelectorAll('tr');
|
||||||
|
let visibleCount = 0;
|
||||||
|
let bannedCount = 0;
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const text = row.textContent.toLowerCase();
|
||||||
|
const isBanned = row.querySelector('a[href*="/members/"]') &&
|
||||||
|
row.querySelector('a[href*="/members/"]').textContent.includes('🚫');
|
||||||
|
|
||||||
|
const matchesSearch = text.includes(searchTerm);
|
||||||
|
const matchesFilter = !showBannedOnly || isBanned;
|
||||||
|
|
||||||
|
const shouldShow = matchesSearch && matchesFilter;
|
||||||
|
row.style.display = shouldShow ? '' : 'none';
|
||||||
|
|
||||||
|
if (shouldShow) {
|
||||||
|
visibleCount++;
|
||||||
|
if (isBanned) bannedCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update count display
|
||||||
|
if (showBannedOnly) {
|
||||||
|
countDisplay.textContent = `Showing ${bannedCount} banned users`;
|
||||||
|
} else {
|
||||||
|
countDisplay.textContent = `${visibleCount} users (${bannedCount} banned)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add search functionality
|
||||||
|
searchInput.addEventListener('input', applyFilters);
|
||||||
|
|
||||||
|
// Add banned filter functionality
|
||||||
|
bannedFilterButton.addEventListener('click', () => {
|
||||||
|
showBannedOnly = !showBannedOnly;
|
||||||
|
|
||||||
|
if (showBannedOnly) {
|
||||||
|
bannedFilterButton.textContent = 'Show All Users';
|
||||||
|
bannedFilterButton.classList.add('active');
|
||||||
|
bannedFilterButton.style.backgroundColor = '#dc2626';
|
||||||
|
bannedFilterButton.style.color = 'white';
|
||||||
|
} else {
|
||||||
|
bannedFilterButton.textContent = 'Show Banned Only';
|
||||||
|
bannedFilterButton.classList.remove('active');
|
||||||
|
bannedFilterButton.style.backgroundColor = '#dc262620';
|
||||||
|
bannedFilterButton.style.color = '#dc2626';
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize filters to set up count display
|
||||||
|
applyFilters();
|
||||||
|
|
||||||
|
// Assemble table
|
||||||
|
table.appendChild(thead);
|
||||||
|
table.appendChild(tbody);
|
||||||
|
|
||||||
|
// Add table to wrapper
|
||||||
|
tableWrapper.appendChild(table);
|
||||||
|
|
||||||
|
// Add elements to container
|
||||||
|
tableContainer.appendChild(searchFilterContainer);
|
||||||
|
tableContainer.appendChild(tableWrapper);
|
||||||
|
|
||||||
|
// Sorting functionality
|
||||||
|
let currentSort = { column: null, direction: 'asc' };
|
||||||
|
|
||||||
|
function sortTable(column, direction) {
|
||||||
|
console.log('Sorting by:', column, direction); // Debug log
|
||||||
|
|
||||||
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||||
|
console.log('Found rows:', rows.length); // Debug log
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
console.log('No rows to sort');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
let aValue, bValue;
|
||||||
|
|
||||||
|
if (column === 'username') {
|
||||||
|
const aLink = a.querySelector('a');
|
||||||
|
const bLink = b.querySelector('a');
|
||||||
|
aValue = aLink ? aLink.textContent.trim().toLowerCase() : '';
|
||||||
|
bValue = bLink ? bLink.textContent.trim().toLowerCase() : '';
|
||||||
|
} else if (column === 'type') {
|
||||||
|
aValue = a.cells[5] ? a.cells[5].textContent.trim().toLowerCase() : '';
|
||||||
|
bValue = b.cells[5] ? b.cells[5].textContent.trim().toLowerCase() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Comparing:', aValue, 'vs', bValue); // Debug log
|
||||||
|
|
||||||
|
if (direction === 'asc') {
|
||||||
|
return aValue.localeCompare(bValue);
|
||||||
|
} else {
|
||||||
|
return bValue.localeCompare(aValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear tbody and re-append sorted rows
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
rows.forEach(row => tbody.appendChild(row));
|
||||||
|
|
||||||
|
// Update sort icons
|
||||||
|
document.querySelectorAll('[id$="-sort-icon"]').forEach(icon => {
|
||||||
|
icon.textContent = '↕';
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortIcon = document.getElementById(`${column}-sort-icon`);
|
||||||
|
if (sortIcon) {
|
||||||
|
sortIcon.textContent = direction === 'asc' ? '↑' : '↓';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Sorting completed'); // Debug log
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click event listeners for sortable columns
|
||||||
|
console.log('Adding click listener to username header');
|
||||||
|
usernameHeader.addEventListener('click', (e) => {
|
||||||
|
console.log('Username header clicked');
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentSort.column === 'username') {
|
||||||
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
currentSort.column = 'username';
|
||||||
|
currentSort.direction = 'asc';
|
||||||
|
}
|
||||||
|
console.log('Current sort:', currentSort);
|
||||||
|
sortTable(currentSort.column, currentSort.direction);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add hover effects for username header
|
||||||
|
usernameHeader.addEventListener('mouseenter', function() {
|
||||||
|
this.style.backgroundColor = '#1a1a1a';
|
||||||
|
});
|
||||||
|
usernameHeader.addEventListener('mouseleave', function() {
|
||||||
|
this.style.backgroundColor = '#101113';
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Adding click listener to type header');
|
||||||
|
typeHeader.addEventListener('click', (e) => {
|
||||||
|
console.log('Type header clicked');
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentSort.column === 'type') {
|
||||||
|
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
|
||||||
|
} else {
|
||||||
|
currentSort.column = 'type';
|
||||||
|
currentSort.direction = 'asc';
|
||||||
|
}
|
||||||
|
console.log('Current sort:', currentSort);
|
||||||
|
sortTable(currentSort.column, currentSort.direction);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add hover effects for type header
|
||||||
|
typeHeader.addEventListener('mouseenter', function() {
|
||||||
|
this.style.backgroundColor = '#1a1a1a';
|
||||||
|
});
|
||||||
|
typeHeader.addEventListener('mouseleave', function() {
|
||||||
|
this.style.backgroundColor = '#101113';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Replace overlay content
|
||||||
|
overlay.innerHTML = '';
|
||||||
|
overlay.appendChild(tableContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for overlay to load and then transform it
|
||||||
|
function waitForOverlay() {
|
||||||
|
const overlay = document.querySelector('.overlay-content');
|
||||||
|
if (overlay) {
|
||||||
|
createTable();
|
||||||
|
} else {
|
||||||
|
// Check again in 100ms
|
||||||
|
setTimeout(waitForOverlay, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start monitoring for overlay
|
||||||
|
waitForOverlay();
|
||||||
|
|
||||||
|
// Also listen for overlay events in case it loads dynamically
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('js-overlayClose') ||
|
||||||
|
e.target.closest('.js-overlayClose')) {
|
||||||
|
// Overlay is closing, wait for it to reopen
|
||||||
|
setTimeout(waitForOverlay, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
Loading…
x
Reference in New Issue
Block a user