Refactor TableManager to enhance table rendering logic and improve status indicator display

This commit is contained in:
fynks 2025-09-30 17:44:52 +05:00
parent 0d400c3134
commit 210ce287ef
2 changed files with 217 additions and 73 deletions

114
dist/css/styles.css vendored
View file

@ -160,10 +160,7 @@
contain: layout paint style
}
.loading-spinner,
.status-indicator svg,
.table-check,
.table-cross {
.loading-spinner {
will-change: transform;
backface-visibility: hidden
}
@ -318,6 +315,26 @@ a:focus-visible {
box-shadow: 0 1px 0 0 rgba(255, 255, 255, .1), 0 1px 3px 0 rgba(0, 0, 0, .02), 0 4px 8px 0 rgba(0, 0, 0, .04)
}
@media (max-width:900px) {
.site-header {
backdrop-filter: none;
-webkit-backdrop-filter: none;
background: rgba(255, 255, 255, .97)
}
[data-theme="dark"] .site-header {
background: rgba(15, 23, 42, .97)
}
.site-header.is-scrolled {
box-shadow: 0 6px 14px rgba(15, 23, 42, .08)
}
[data-theme="dark"] .site-header.is-scrolled {
box-shadow: 0 6px 14px rgba(0, 0, 0, .4)
}
}
[data-theme="dark"] .site-header {
background: rgba(15, 23, 42, 0)
}
@ -1774,6 +1791,67 @@ tr:hover {
background: color-mix(in srgb, var(--bg-secondary) 80%, transparent)
}
.status-cell,
.support-status {
text-align: center;
}
.status-indicator {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.9rem;
height: 1.9rem;
border-radius: var(--radius-full);
border: 1.5px solid var(--border-secondary);
background: var(--bg-primary);
color: var(--text-secondary);
font-weight: var(--font-semibold);
letter-spacing: .02em;
transition: transform var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
.status-indicator::before {
content: attr(data-symbol);
line-height: 1;
font-size: .9rem;
}
.status-indicator[data-supported="true"] {
background: rgba(16, 185, 129, .18);
border-color: rgba(16, 185, 129, .45);
color: var(--success);
}
[data-theme="dark"] .status-indicator[data-supported="true"] {
background: rgba(16, 185, 129, .25);
border-color: rgba(16, 185, 129, .55);
}
.status-indicator[data-supported="false"] {
background: rgba(239, 68, 68, .12);
border-color: rgba(239, 68, 68, .3);
color: var(--error);
}
[data-theme="dark"] .status-indicator[data-supported="false"] {
background: rgba(239, 68, 68, .18);
border-color: rgba(239, 68, 68, .35);
}
.status-cell:hover .status-indicator,
.support-status:hover .status-indicator {
transform: scale(1.05);
}
.empty-state-row {
text-align: center;
padding: var(--space-2xl) !important;
color: var(--text-secondary);
background: var(--bg-secondary);
font-style: italic;
}
.pricing-table .service-name {
color: inherit
}
@ -1803,20 +1881,6 @@ tr:hover {
font-family: 'Courier New', Courier, monospace
}
.table-check,
.table-cross {
padding: .2em;
border-radius: 50px
}
.table-check {
background-color: rgba(90, 221, 90, .568)
}
.table-cross {
background-color: #db747465
}
.search-section {
margin-bottom: var(--space-4xl)
}
@ -1924,6 +1988,20 @@ tr:hover {
opacity: 1
}
@media (max-width:768px) {
.status-card,
.referral-card {
box-shadow: none;
transform: none !important;
transition: background var(--transition-fast), border-color var(--transition-fast)
}
.status-card::before,
.referral-card::before {
display: none
}
}
.status-header {
display: flex;
align-items: center;

176
dist/js/app.js vendored
View file

@ -144,11 +144,13 @@ class TableManager {
currentSort: { column: null, direction: "asc" },
currentPage: 1,
services: [], // Store services array for indexed format
columns: [],
isIndexedFormat: false // Flag to track data format
};
this.handleSearch = Utils.debounce(this._performSearch.bind(this), 250);
this.handleSort = this._performSort.bind(this);
this.renderToken = null;
this._init();
}
@ -220,6 +222,7 @@ class TableManager {
const firstHostKey = Object.keys(processedData)[0];
const columns = firstHostKey ? Object.keys(processedData[firstHostKey]) : [];
this.state.columns = columns;
const tableId = this.elements.container.id.replace("-container", "");
const fragment = document.createDocumentFragment();
@ -236,7 +239,6 @@ class TableManager {
table.appendChild(thead);
const tbody = document.createElement("tbody");
tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
table.appendChild(tbody);
wrapper.appendChild(table);
@ -246,6 +248,7 @@ class TableManager {
this.elements.container.appendChild(fragment);
this._attachTableEvents();
this._renderTableBody(this.state.filteredData, columns);
}
_generateTableHeader(columns) {
@ -269,33 +272,105 @@ class TableManager {
`;
}
_generateTableRows(data, columns) {
const rows = [];
for (const [host, hostData] of Object.entries(data)) {
rows.push(`
<tr data-host="${host.toLowerCase()}" role="row">
<td class="service-cell" role="gridcell">
<div class="service-info">
<span class="service-name">${host}</span>
</div>
</td>
${columns.map(column => {
const isSupported = hostData[column] === "✅";
return `
<td class="status-cell" role="gridcell" data-status="${hostData[column]}">
<span class="status-indicator ${isSupported ? "supported" : "not-supported"}" aria-label="${isSupported ? "Supported" : "Not supported"}">
${isSupported ?
'<svg class="table-check" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22,4 12,14.01 9,11.01"></polyline></svg>' :
'<svg class="table-cross" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>'
}
</span>
</td>
`;
}).join("")}
</tr>
`);
_renderTableBody(data, columns = this.state.columns) {
const table = this.elements.container.querySelector("table");
const tbody = table?.querySelector("tbody");
if (!tbody) return;
const effectiveColumns = Array.isArray(columns) && columns.length ? columns : this.state.columns;
const entries = Object.entries(data);
const token = Symbol("render");
this.renderToken = token;
tbody.textContent = "";
if (!entries.length) {
const emptyRow = document.createElement("tr");
const emptyCell = document.createElement("td");
emptyCell.colSpan = (effectiveColumns?.length || 0) + 1;
emptyCell.className = "empty-state-row";
emptyCell.textContent = "No services match your current filters.";
emptyRow.appendChild(emptyCell);
tbody.appendChild(emptyRow);
return;
}
return rows.join("");
let index = 0;
const baseChunk = Math.max(20, 60 - Math.max(0, effectiveColumns.length - 4) * 5);
const isCompact = typeof window !== "undefined" && typeof window.matchMedia === "function"
? window.matchMedia("(max-width: 640px)").matches
: false;
const chunkSize = isCompact ? Math.max(10, Math.floor(baseChunk / 1.5)) : baseChunk;
const schedule = (cb) => {
if (typeof requestIdleCallback === "function") {
requestIdleCallback(cb, { timeout: 160 });
} else {
requestAnimationFrame(cb);
}
};
const renderChunk = () => {
if (this.renderToken !== token) return;
const fragment = document.createDocumentFragment();
const limit = Math.min(entries.length, index + chunkSize);
for (; index < limit; index++) {
const [host, hostData] = entries[index];
fragment.appendChild(this._createTableRow(host, hostData, effectiveColumns));
}
tbody.appendChild(fragment);
if (index < entries.length) {
schedule(renderChunk);
}
};
schedule(renderChunk);
}
_createTableRow(host, hostData, columns) {
const row = document.createElement("tr");
row.dataset.host = host.toLowerCase();
row.setAttribute("role", "row");
const serviceCell = document.createElement("td");
serviceCell.className = "service-cell";
serviceCell.setAttribute("role", "gridcell");
const serviceInfo = document.createElement("div");
serviceInfo.className = "service-info";
const serviceName = document.createElement("span");
serviceName.className = "service-name";
serviceName.textContent = host;
serviceInfo.appendChild(serviceName);
serviceCell.appendChild(serviceInfo);
row.appendChild(serviceCell);
columns.forEach((column) => {
const statusCell = document.createElement("td");
statusCell.className = "status-cell";
statusCell.dataset.status = hostData[column] || "";
const isSupported = hostData[column] === "✅";
statusCell.dataset.supported = isSupported ? "true" : "false";
statusCell.setAttribute("role", "gridcell");
const indicator = document.createElement("span");
indicator.className = "status-indicator";
indicator.dataset.supported = isSupported ? "true" : "false";
indicator.dataset.symbol = isSupported ? "✓" : "✕";
indicator.setAttribute("aria-hidden", "true");
const srText = document.createElement("span");
srText.className = "visually-hidden";
srText.textContent = isSupported ? "Supported" : "Not supported";
statusCell.appendChild(indicator);
statusCell.appendChild(srText);
row.appendChild(statusCell);
});
return row;
}
_attachTableEvents() {
@ -423,15 +498,7 @@ class TableManager {
}
_updateTableContent() {
const tbody = this.elements.container.querySelector("tbody");
if (!tbody) return;
const firstHostKey = Object.keys(this.state.currentData)[0];
const columns = firstHostKey ? Object.keys(this.state.currentData[firstHostKey]) : [];
requestAnimationFrame(() => {
tbody.innerHTML = this._generateTableRows(this.state.filteredData, columns);
});
this._renderTableBody(this.state.filteredData, this.state.columns);
}
}
@ -605,20 +672,27 @@ class ComparisonManager {
_generateComparisonRows(service1, service2) {
const rows = [];
const checkIcon = '<svg class="table-check" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22,4 12,14.01 9,11.01"></polyline></svg>';
const crossIcon = '<svg class="table-cross" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>';
// Get all hosts (keys from the original data structure)
const hosts = this.isIndexedFormat ? Object.keys(this.data.supported) : Object.keys(this.data);
const getStatusCell = (supported) => {
const supportedClass = supported ? "supported" : "not-supported";
const supportedFlag = supported ? "true" : "false";
const symbol = supported ? "✓" : "✕";
return `
<td class="support-status ${supportedClass}" data-supported="${supportedFlag}">
<span class="status-indicator" data-supported="${supportedFlag}" data-symbol="${symbol}" aria-hidden="true"></span>
<span class="visually-hidden">${supported ? "Supported" : "Not supported"}</span>
</td>
`;
};
for (const host of hosts) {
const hostData = this._getHostData(host);
const supported1 = hostData[service1] === "✅";
const supported2 = hostData[service2] === "✅";
let statusClass = "neither-supported";
let statusText = "Neither";
if (supported1 && supported2) {
statusClass = "both-supported";
statusText = "Both";
@ -629,25 +703,17 @@ class ComparisonManager {
statusClass = "service2-only";
statusText = `${service2} only`;
}
rows.push(`
<tr class="comparison-row ${statusClass}" data-status="${statusClass}">
<td class="service-name">${host}</td>
<td class="support-status ${supported1 ? "supported" : "not-supported"}">
<span class="status-indicator" aria-label="${supported1 ? "Supported" : "Not supported"}">
${supported1 ? checkIcon : crossIcon}
</span>
</td>
<td class="support-status ${supported2 ? "supported" : "not-supported"}">
<span class="status-indicator" aria-label="${supported2 ? "Supported" : "Not supported"}">
${supported2 ? checkIcon : crossIcon}
</span>
</td>
${getStatusCell(supported1)}
${getStatusCell(supported2)}
<td class="status-text"><span class="status-badge ${statusClass}">${statusText}</span></td>
</tr>
`);
}
return rows.join("");
}