feat(worker): implement search worker for fuzzy matching and data filtering
- Added a new search worker to offload heavy search/filter operations from the main thread. - Implemented fuzzy matching using Levenshtein distance and hostname normalization. - Added functions to extract hostnames from URLs and perform similarity calculations. - Included data filtering and sorting capabilities based on specified criteria. - Handled various message actions for search, filter, sort, and similarity calculations.
This commit is contained in:
parent
2f28fe71a4
commit
34856ab44b
4 changed files with 1622 additions and 86 deletions
345
dist/css/styles.css
vendored
345
dist/css/styles.css
vendored
|
|
@ -4507,4 +4507,347 @@ select:hover {
|
||||||
.nav-link:hover, .btn:hover, .status-card:hover, .benefit-card:hover, .speed-card:hover, .referral-card:hover, .note-card:hover, .resource-item:hover {
|
.nav-link:hover, .btn:hover, .status-card:hover, .benefit-card:hover, .speed-card:hover, .referral-card:hover, .note-card:hover, .resource-item:hover {
|
||||||
transform:none
|
transform:none
|
||||||
}
|
}
|
||||||
}
|
}/* ===== ACCESSIBILITY ENHANCEMENTS ===== */
|
||||||
|
.visually-hidden {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#a11y-announcer {
|
||||||
|
position: absolute;
|
||||||
|
left: -10000px;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced focus styles for keyboard navigation */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 3px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: outline-offset 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus-visible,
|
||||||
|
a:focus-visible,
|
||||||
|
input:focus-visible,
|
||||||
|
select:focus-visible,
|
||||||
|
textarea:focus-visible {
|
||||||
|
outline: 3px solid var(--primary);
|
||||||
|
outline-offset: 3px;
|
||||||
|
box-shadow: 0 0 0 6px rgba(99, 102, 241, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keyboard navigation for tables */
|
||||||
|
table tbody tr:focus {
|
||||||
|
outline: 3px solid var(--primary);
|
||||||
|
outline-offset: -3px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr:focus .service-name {
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr[tabindex="0"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE TABLE ENHANCEMENTS ===== */
|
||||||
|
|
||||||
|
/* Mobile card layout */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.table-wrapper.mobile-view table {
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view tbody {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view tr {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view tr:hover,
|
||||||
|
.table-wrapper.mobile-view tr:focus {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view td {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-sm) 0;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view td:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-right: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view .service-cell {
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
padding-bottom: var(--space-md);
|
||||||
|
border-bottom: 2px solid var(--border-primary) !important;
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view .service-cell::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view .status-cell {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrapper.mobile-view .status-indicator {
|
||||||
|
font-size: var(--text-xl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet optimizations */
|
||||||
|
@media (min-width: 768px) and (max-width: 1024px) {
|
||||||
|
.table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: var(--space-sm) var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ENHANCED LOADING STATES ===== */
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: grayscale(0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner animation */
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid var(--border-primary);
|
||||||
|
border-top-color: var(--primary);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search results animation */
|
||||||
|
.search-results {
|
||||||
|
animation: slideDown 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== IMPROVED TOUCH TARGETS ===== */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
button,
|
||||||
|
a,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: var(--space-lg) var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: var(--space-lg) var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== DARK MODE ADJUSTMENTS ===== */
|
||||||
|
[data-theme="dark"] table tbody tr:focus {
|
||||||
|
background-color: rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] *:focus-visible {
|
||||||
|
outline-color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== REDUCED MOTION SUPPORT ===== */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.search-results,
|
||||||
|
.table-wrapper.mobile-view tr {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
animation-duration: 2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr:focus {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== HIGH CONTRAST MODE ===== */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
*:focus-visible {
|
||||||
|
outline-width: 4px;
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr:focus {
|
||||||
|
outline-width: 4px;
|
||||||
|
outline-offset: -4px;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== PRINT STYLES ===== */
|
||||||
|
@media print {
|
||||||
|
.visually-hidden,
|
||||||
|
#a11y-announcer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tbody tr:focus {
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ERROR STATE IMPROVEMENTS ===== */
|
||||||
|
.error-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--space-3xl);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state[role="alert"] {
|
||||||
|
border: 2px solid var(--error);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state .btn {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== LOAD ALL BUTTON ENHANCEMENTS ===== */
|
||||||
|
.load-all-container {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-all-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-all-btn svg {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-all-btn:hover:not(:disabled) svg {
|
||||||
|
transform: translateY(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== COMPARISON TABLE MOBILE IMPROVEMENTS ===== */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.comparison-table {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-header {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-stats {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
1
dist/index.html
vendored
1
dist/index.html
vendored
|
|
@ -425,7 +425,6 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="./css/styles.css">
|
<link rel="stylesheet" href="./css/styles.css">
|
||||||
|
|
||||||
<script src="js/app.js" defer></script>
|
<script src="js/app.js" defer></script>
|
||||||
|
|
||||||
<!-- Verification Tags -->
|
<!-- Verification Tags -->
|
||||||
|
|
|
||||||
1085
dist/js/app.js
vendored
1085
dist/js/app.js
vendored
File diff suppressed because it is too large
Load diff
277
dist/workers/search.worker.js
vendored
Normal file
277
dist/workers/search.worker.js
vendored
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
/**
|
||||||
|
* Search Worker - Offloads heavy search/filter operations from main thread
|
||||||
|
* Handles fuzzy matching, hostname extraction, and data filtering
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Normalize hostname for fuzzy matching
|
||||||
|
const normalizeHostname = (hostname) => {
|
||||||
|
if (!hostname) return '';
|
||||||
|
|
||||||
|
return hostname
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]/g, '') // Remove non-alphanumeric
|
||||||
|
.replace(/^(www|ftp|cdn|dl|download|file|files|upload|uploads)\d*/g, '') // Remove common prefixes
|
||||||
|
.replace(/\d+$/g, ''); // Remove trailing numbers
|
||||||
|
};
|
||||||
|
|
||||||
|
// Levenshtein distance algorithm
|
||||||
|
const levenshteinDistance = (str1, str2) => {
|
||||||
|
const matrix = [];
|
||||||
|
|
||||||
|
for (let i = 0; i <= str2.length; i++) {
|
||||||
|
matrix[i] = [i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j <= str1.length; j++) {
|
||||||
|
matrix[0][j] = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= str2.length; i++) {
|
||||||
|
for (let j = 1; j <= str1.length; j++) {
|
||||||
|
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
||||||
|
matrix[i][j] = matrix[i - 1][j - 1];
|
||||||
|
} else {
|
||||||
|
matrix[i][j] = Math.min(
|
||||||
|
matrix[i - 1][j - 1] + 1, // substitution
|
||||||
|
matrix[i][j - 1] + 1, // insertion
|
||||||
|
matrix[i - 1][j] + 1 // deletion
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matrix[str2.length][str1.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate similarity score between two strings
|
||||||
|
const calculateSimilarity = (str1, str2) => {
|
||||||
|
const s1 = normalizeHostname(str1);
|
||||||
|
const s2 = normalizeHostname(str2);
|
||||||
|
|
||||||
|
if (!s1 || !s2) return 0;
|
||||||
|
|
||||||
|
// Exact match
|
||||||
|
if (s1 === s2) return 100;
|
||||||
|
|
||||||
|
// One contains the other
|
||||||
|
if (s1.includes(s2) || s2.includes(s1)) {
|
||||||
|
const longer = Math.max(s1.length, s2.length);
|
||||||
|
const shorter = Math.min(s1.length, s2.length);
|
||||||
|
return Math.round((shorter / longer) * 95);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if they start similarly
|
||||||
|
const minLength = Math.min(s1.length, s2.length);
|
||||||
|
let matchingChars = 0;
|
||||||
|
for (let i = 0; i < minLength; i++) {
|
||||||
|
if (s1[i] === s2[i]) {
|
||||||
|
matchingChars++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingChars >= 3) {
|
||||||
|
return Math.round((matchingChars / Math.max(s1.length, s2.length)) * 85);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levenshtein-like distance for fuzzy matching
|
||||||
|
const maxLength = Math.max(s1.length, s2.length);
|
||||||
|
if (maxLength === 0) return 100;
|
||||||
|
|
||||||
|
let distance = levenshteinDistance(s1, s2);
|
||||||
|
return Math.max(0, Math.round((1 - distance / maxLength) * 80));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract hostname from URL
|
||||||
|
const extractHostnameFromURL = (input) => {
|
||||||
|
if (!input || typeof input !== 'string') return null;
|
||||||
|
|
||||||
|
let normalized = input.trim();
|
||||||
|
|
||||||
|
// If it doesn't start with a protocol, try to detect if it's a URL
|
||||||
|
if (!normalized.match(/^https?:\/\//i)) {
|
||||||
|
// Check if it looks like a URL (contains dots and domain-like patterns)
|
||||||
|
if (normalized.match(/[a-z0-9-]+\.[a-z]{2,}/i)) {
|
||||||
|
// Try to extract domain-like pattern
|
||||||
|
const domainMatch = normalized.match(/([a-z0-9-]+\.(?:[a-z]{2,}\.)?[a-z]{2,})/i);
|
||||||
|
if (domainMatch) {
|
||||||
|
normalized = 'https://' + domainMatch[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not a URL, return the input as-is for regular search
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Replace common typos in protocol
|
||||||
|
normalized = normalized.replace(/^https?[:;.]+\/*/i, 'https://');
|
||||||
|
|
||||||
|
// Parse the URL
|
||||||
|
const url = new URL(normalized);
|
||||||
|
let hostname = url.hostname.toLowerCase();
|
||||||
|
|
||||||
|
// Remove 'www.' prefix
|
||||||
|
hostname = hostname.replace(/^www\d*\./i, '');
|
||||||
|
|
||||||
|
// Handle subdomains - extract main domain
|
||||||
|
const parts = hostname.split('.');
|
||||||
|
|
||||||
|
// If it's a known multi-level TLD (co.uk, com.au, etc.)
|
||||||
|
const multiLevelTLDs = ['co.uk', 'com.au', 'co.nz', 'co.za', 'com.br', 'co.jp'];
|
||||||
|
const lastTwoParts = parts.slice(-2).join('.');
|
||||||
|
|
||||||
|
if (multiLevelTLDs.includes(lastTwoParts)) {
|
||||||
|
// Keep last 3 parts (domain + multi-level TLD)
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
hostname = parts.slice(-3).join('.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Keep last 2 parts (domain + TLD)
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
hostname = parts.slice(-2).join('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract just the domain name without TLD for searching
|
||||||
|
const domainName = hostname.split('.')[0];
|
||||||
|
|
||||||
|
return domainName;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// If URL parsing fails, try to extract domain manually
|
||||||
|
const manualMatch = normalized.match(/\/\/([^/:?#]+)/);
|
||||||
|
if (manualMatch) {
|
||||||
|
let hostname = manualMatch[1].toLowerCase();
|
||||||
|
hostname = hostname.replace(/^www\d*\./i, '');
|
||||||
|
const domainName = hostname.split('.')[0];
|
||||||
|
return domainName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perform search operation
|
||||||
|
const performSearch = (data, searchTerm) => {
|
||||||
|
const isURL = extractHostnameFromURL(searchTerm);
|
||||||
|
const term = isURL || searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
if (isURL) {
|
||||||
|
// Use fuzzy matching for URL-based searches
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
for (const [host, hostData] of Object.entries(data)) {
|
||||||
|
const similarity = calculateSimilarity(host, term);
|
||||||
|
|
||||||
|
// Include if similarity is above threshold (60%)
|
||||||
|
if (similarity >= 60) {
|
||||||
|
matches.push({
|
||||||
|
host,
|
||||||
|
hostData,
|
||||||
|
similarity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by similarity score (highest first)
|
||||||
|
matches.sort((a, b) => b.similarity - a.similarity);
|
||||||
|
|
||||||
|
// Convert back to object
|
||||||
|
return {
|
||||||
|
filtered: Object.fromEntries(
|
||||||
|
matches.map(({ host, hostData }) => [host, hostData])
|
||||||
|
),
|
||||||
|
isURLSearch: true,
|
||||||
|
extractedHostname: term
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Regular text search (exact substring matching)
|
||||||
|
return {
|
||||||
|
filtered: Object.fromEntries(
|
||||||
|
Object.entries(data).filter(([host]) =>
|
||||||
|
host.toLowerCase().includes(term)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
isURLSearch: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter data based on criteria
|
||||||
|
const filterData = (data, filters) => {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(data).filter(([host, hostData]) => {
|
||||||
|
return Object.entries(filters).every(([key, value]) => {
|
||||||
|
if (value === null || value === undefined) return true;
|
||||||
|
return hostData[key] === value;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort data
|
||||||
|
const sortData = (data, column, direction) => {
|
||||||
|
const entries = Object.entries(data);
|
||||||
|
|
||||||
|
entries.sort(([hostA, dataA], [hostB, dataB]) => {
|
||||||
|
let valueA, valueB;
|
||||||
|
|
||||||
|
if (column === 'service') {
|
||||||
|
valueA = hostA.toLowerCase();
|
||||||
|
valueB = hostB.toLowerCase();
|
||||||
|
} else {
|
||||||
|
valueA = dataA[column] === '✅' ? 1 : 0;
|
||||||
|
valueB = dataB[column] === '✅' ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueA < valueB) return direction === 'asc' ? -1 : 1;
|
||||||
|
if (valueA > valueB) return direction === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.fromEntries(entries);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Message handler
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
const { action, data, ...params } = event.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'search':
|
||||||
|
result = performSearch(data, params.term);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'filter':
|
||||||
|
result = filterData(data, params.filters);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sort':
|
||||||
|
result = sortData(data, params.column, params.direction);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'calculate-similarity':
|
||||||
|
result = calculateSimilarity(params.str1, params.str2);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown action: ${action}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
success: true,
|
||||||
|
action,
|
||||||
|
result
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
success: false,
|
||||||
|
action,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue