Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced Data Table</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| /* Custom styles */ | |
| .table-container { | |
| overflow-x: auto; | |
| max-width: 100%; | |
| } | |
| .table-wrapper { | |
| position: relative; | |
| } | |
| .table-header { | |
| position: sticky; | |
| top: 0; | |
| background-color: white; | |
| z-index: 10; | |
| } | |
| .resize-handle { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| width: 5px; | |
| height: 100%; | |
| background-color: #e5e7eb; | |
| cursor: col-resize; | |
| } | |
| .resize-handle:hover { | |
| background-color: #3b82f6; | |
| } | |
| .draggable-header { | |
| cursor: move; | |
| } | |
| .column-options { | |
| position: absolute; | |
| right: 0; | |
| top: 100%; | |
| z-index: 20; | |
| background: white; | |
| border: 1px solid #e5e7eb; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| min-width: 200px; | |
| } | |
| .filter-panel { | |
| position: absolute; | |
| right: 0; | |
| top: 100%; | |
| z-index: 20; | |
| background: white; | |
| border: 1px solid #e5e7eb; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| min-width: 250px; | |
| padding: 1rem; | |
| } | |
| .ellipsis-text { | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| max-width: 300px; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| background: #333; | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| z-index: 100; | |
| display: none; | |
| } | |
| .sort-icon { | |
| transition: transform 0.2s; | |
| } | |
| .sort-asc { | |
| transform: rotate(180deg); | |
| } | |
| .file-drop-area { | |
| border: 2px dashed #cbd5e1; | |
| border-radius: 0.5rem; | |
| padding: 2rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .file-drop-area.active { | |
| border-color: #3b82f6; | |
| background-color: #eff6ff; | |
| } | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| z-index: 1000; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| .modal-content { | |
| background-color: white; | |
| padding: 2rem; | |
| border-radius: 0.5rem; | |
| max-width: 90%; | |
| max-height: 90%; | |
| overflow: auto; | |
| } | |
| .column-menu { | |
| position: absolute; | |
| background: white; | |
| border: 1px solid #e5e7eb; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| min-width: 180px; | |
| z-index: 30; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50 p-4"> | |
| <div class="max-w-7xl mx-auto"> | |
| <h1 class="text-2xl font-bold text-gray-800 mb-6">Advanced Data Table</h1> | |
| <!-- Controls Section --> | |
| <div class="bg-white rounded-lg shadow p-4 mb-6"> | |
| <div class="flex flex-wrap gap-4 items-center justify-between mb-4"> | |
| <div class="flex gap-2"> | |
| <button id="uploadBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-upload"></i> Upload JSON | |
| </button> | |
| <button id="downloadBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-download"></i> Download Data | |
| </button> | |
| <button id="pasteBtn" class="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-paste"></i> Paste from Clipboard | |
| </button> | |
| <button id="loadConfigBtn" class="bg-yellow-500 hover:bg-yellow-600 text-white px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-cog"></i> Load Config | |
| </button> | |
| <button id="saveConfigBtn" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-save"></i> Save Config | |
| </button> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button id="toggleFiltersBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-filter"></i> Toggle Filters | |
| </button> | |
| <button id="toggleColumnsBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded flex items-center gap-2"> | |
| <i class="fas fa-columns"></i> Columns | |
| </button> | |
| </div> | |
| </div> | |
| <!-- File Drop Area --> | |
| <div id="fileDropArea" class="file-drop-area mb-4"> | |
| <div class="flex flex-col items-center justify-center"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i> | |
| <p class="text-gray-600">Drag & drop your JSON file here</p> | |
| <p class="text-sm text-gray-500 mt-1">or click to browse files</p> | |
| </div> | |
| <input type="file" id="fileInput" accept=".json" class="hidden"> | |
| </div> | |
| <!-- URL Input --> | |
| <div class="flex gap-2 mb-4"> | |
| <input type="text" id="jsonUrl" placeholder="Enter JSON URL" class="flex-1 border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <button id="loadUrlBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"> | |
| Load from URL | |
| </button> | |
| </div> | |
| <!-- Search Input --> | |
| <div class="relative mb-4"> | |
| <input type="text" id="globalSearch" placeholder="Search across all columns..." class="w-full border border-gray-300 rounded px-3 py-2 pl-10 focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </div> | |
| </div> | |
| <!-- Filter Panel --> | |
| <div id="filterPanel" class="filter-panel hidden bg-white rounded-lg shadow mb-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="font-bold text-lg">Filters</h3> | |
| <button id="closeFiltersBtn" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div id="filterControls" class="space-y-4"> | |
| <!-- Filters will be dynamically added here --> | |
| </div> | |
| <div class="flex justify-end gap-2 mt-4"> | |
| <button id="applyFiltersBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded"> | |
| Apply Filters | |
| </button> | |
| <button id="resetFiltersBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-800 px-4 py-2 rounded"> | |
| Reset | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Column Options Panel --> | |
| <div id="columnOptionsPanel" class="column-options hidden bg-white rounded-lg shadow"> | |
| <div class="p-3"> | |
| <h3 class="font-bold mb-2">Visible Columns</h3> | |
| <div id="columnCheckboxes" class="space-y-2"> | |
| <!-- Column checkboxes will be dynamically added here --> | |
| </div> | |
| <button id="resetColumnsBtn" class="mt-3 text-blue-500 hover:text-blue-700 text-sm"> | |
| Reset to Default | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Table Container --> | |
| <div class="table-container bg-white rounded-lg shadow"> | |
| <div class="table-wrapper"> | |
| <table id="dataTable" class="w-full border-collapse"> | |
| <thead class="table-header"> | |
| <tr id="tableHeader" class="bg-gray-100 text-left"> | |
| <!-- Table headers will be dynamically added here --> | |
| </tr> | |
| </thead> | |
| <tbody id="tableBody"> | |
| <!-- Table data will be dynamically added here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Pagination --> | |
| <div id="pagination" class="flex items-center justify-between p-4 border-t border-gray-200"> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-sm text-gray-600">Rows per page:</span> | |
| <select id="rowsPerPage" class="border border-gray-300 rounded px-2 py-1 text-sm"> | |
| <option value="10">10</option> | |
| <option value="25">25</option> | |
| <option value="50">50</option> | |
| <option value="100">100</option> | |
| </select> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <button id="firstPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled> | |
| <i class="fas fa-angle-double-left"></i> | |
| </button> | |
| <button id="prevPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled> | |
| <i class="fas fa-angle-left"></i> | |
| </button> | |
| <span id="pageInfo" class="text-sm text-gray-600">Page 1 of 1</span> | |
| <button id="nextPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled> | |
| <i class="fas fa-angle-right"></i> | |
| </button> | |
| <button id="lastPage" class="px-3 py-1 border rounded disabled:opacity-50" disabled> | |
| <i class="fas fa-angle-double-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Tooltip --> | |
| <div id="tooltip" class="tooltip"></div> | |
| <!-- Modal for full text view --> | |
| <div id="textModal" class="modal"> | |
| <div class="modal-content"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="font-bold text-lg" id="modalTitle">Full Text</h3> | |
| <button id="closeModalBtn" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div id="modalContent" class="max-w-4xl max-h-[70vh] overflow-auto"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // Sample data | |
| const sampleData = [ | |
| { id: 1, name: "John Doe", email: "john.doe@example.com", age: 32, status: "Active", joinDate: "2022-01-15", salary: 75000, department: "Engineering", description: "Senior software engineer with 8 years of experience in web development." }, | |
| { id: 2, name: "Jane Smith", email: "jane.smith@example.com", age: 28, status: "Active", joinDate: "2022-03-22", salary: 68000, department: "Marketing", description: "Digital marketing specialist focused on SEO and content strategy." }, | |
| { id: 3, name: "Robert Johnson", email: "robert.j@example.com", age: 45, status: "Inactive", joinDate: "2021-11-05", salary: 92000, department: "Management", description: "Project manager with extensive experience in agile methodologies." }, | |
| { id: 4, name: "Emily Davis", email: "emily.davis@example.com", age: 31, status: "Active", joinDate: "2022-05-18", salary: 71000, department: "Engineering", description: "Frontend developer specializing in React and Vue.js frameworks." }, | |
| { id: 5, name: "Michael Brown", email: "michael.b@example.com", age: 29, status: "Pending", joinDate: "2022-07-30", salary: 65000, department: "Sales", description: "Sales representative with strong customer relationship skills." }, | |
| { id: 6, name: "Sarah Wilson", email: "sarah.w@example.com", age: 36, status: "Active", joinDate: "2021-09-12", salary: 85000, department: "HR", description: "HR manager responsible for recruitment and employee relations." }, | |
| { id: 7, name: "David Taylor", email: "david.t@example.com", age: 42, status: "Inactive", joinDate: "2020-12-01", salary: 88000, department: "Finance", description: "Financial analyst with expertise in budgeting and forecasting." }, | |
| { id: 8, name: "Jessica Martinez", email: "jessica.m@example.com", age: 27, status: "Active", joinDate: "2022-04-05", salary: 69000, department: "Engineering", description: "Backend developer working primarily with Node.js and Python." }, | |
| { id: 9, name: "Thomas Anderson", email: "thomas.a@example.com", age: 38, status: "Active", joinDate: "2021-06-20", salary: 78000, department: "Product", description: "Product owner with background in UX design and business analysis." }, | |
| { id: 10, name: "Lisa Jackson", email: "lisa.j@example.com", age: 33, status: "Pending", joinDate: "2022-08-15", salary: 72000, department: "Marketing", description: "Social media manager creating engaging content for various platforms." }, | |
| { id: 11, name: "James White", email: "james.w@example.com", age: 40, status: "Active", joinDate: "2020-10-10", salary: 95000, department: "Management", description: "Director of operations overseeing multiple departments and projects." }, | |
| { id: 12, name: "Amanda Harris", email: "amanda.h@example.com", age: 26, status: "Active", joinDate: "2022-02-28", salary: 63000, department: "Sales", description: "Junior sales associate learning the ropes of the business." }, | |
| { id: 13, name: "Daniel Martin", email: "daniel.m@example.com", age: 35, status: "Inactive", joinDate: "2021-04-17", salary: 82000, department: "Engineering", description: "DevOps engineer managing cloud infrastructure and CI/CD pipelines." }, | |
| { id: 14, name: "Jennifer Lee", email: "jennifer.l@example.com", age: 30, status: "Active", joinDate: "2022-06-08", salary: 74000, department: "Product", description: "UX designer creating intuitive interfaces for web and mobile applications." }, | |
| { id: 15, name: "Christopher Walker", email: "chris.w@example.com", age: 44, status: "Active", joinDate: "2020-08-25", salary: 91000, department: "Finance", description: "Senior accountant handling corporate financial statements and audits." } | |
| ]; | |
| // DOM elements | |
| const tableHeader = document.getElementById('tableHeader'); | |
| const tableBody = document.getElementById('tableBody'); | |
| const pagination = document.getElementById('pagination'); | |
| const pageInfo = document.getElementById('pageInfo'); | |
| const firstPageBtn = document.getElementById('firstPage'); | |
| const prevPageBtn = document.getElementById('prevPage'); | |
| const nextPageBtn = document.getElementById('nextPage'); | |
| const lastPageBtn = document.getElementById('lastPage'); | |
| const rowsPerPageSelect = document.getElementById('rowsPerPage'); | |
| const globalSearchInput = document.getElementById('globalSearch'); | |
| const uploadBtn = document.getElementById('uploadBtn'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const pasteBtn = document.getElementById('pasteBtn'); | |
| const loadConfigBtn = document.getElementById('loadConfigBtn'); | |
| const saveConfigBtn = document.getElementById('saveConfigBtn'); | |
| const toggleFiltersBtn = document.getElementById('toggleFiltersBtn'); | |
| const toggleColumnsBtn = document.getElementById('toggleColumnsBtn'); | |
| const filterPanel = document.getElementById('filterPanel'); | |
| const columnOptionsPanel = document.getElementById('columnOptionsPanel'); | |
| const closeFiltersBtn = document.getElementById('closeFiltersBtn'); | |
| const applyFiltersBtn = document.getElementById('applyFiltersBtn'); | |
| const resetFiltersBtn = document.getElementById('resetFiltersBtn'); | |
| const resetColumnsBtn = document.getElementById('resetColumnsBtn'); | |
| const columnCheckboxes = document.getElementById('columnCheckboxes'); | |
| const filterControls = document.getElementById('filterControls'); | |
| const fileDropArea = document.getElementById('fileDropArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const jsonUrl = document.getElementById('jsonUrl'); | |
| const loadUrlBtn = document.getElementById('loadUrlBtn'); | |
| const tooltip = document.getElementById('tooltip'); | |
| const textModal = document.getElementById('textModal'); | |
| const modalContent = document.getElementById('modalContent'); | |
| const modalTitle = document.getElementById('modalTitle'); | |
| const closeModalBtn = document.getElementById('closeModalBtn'); | |
| // State variables | |
| let data = [...sampleData]; | |
| let filteredData = [...data]; | |
| let currentPage = 1; | |
| let rowsPerPage = parseInt(rowsPerPageSelect.value); | |
| let sortColumn = null; | |
| let sortDirection = 'asc'; | |
| let columnConfig = {}; | |
| let activeFilters = {}; | |
| let isDragging = false; | |
| let dragStartX = 0; | |
| let dragStartWidth = 0; | |
| let dragColumnIndex = -1; | |
| let dragColumnElement = null; | |
| let isDraggingColumn = false; | |
| let dragStartColumnX = 0; | |
| let draggedColumnIndex = -1; | |
| let columnOrder = []; | |
| // Initialize the table | |
| function initTable() { | |
| if (data.length === 0) return; | |
| // Initialize column configuration if not already set | |
| if (Object.keys(columnConfig).length === 0) { | |
| const firstItem = data[0]; | |
| columnOrder = Object.keys(firstItem); | |
| Object.keys(firstItem).forEach(key => { | |
| columnConfig[key] = { | |
| visible: true, | |
| width: 200, // Default width | |
| type: detectType(firstItem[key]) | |
| }; | |
| }); | |
| } | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| renderPagination(); | |
| renderColumnOptions(); | |
| renderFilterControls(); | |
| } | |
| // Detect data type for a value | |
| function detectType(value) { | |
| if (typeof value === 'number') return 'number'; | |
| if (typeof value === 'boolean') return 'boolean'; | |
| if (Date.parse(value)) return 'date'; | |
| if (typeof value === 'string') { | |
| // Check if it's an enum (limited distinct values) | |
| const distinctValues = [...new Set(data.map(item => item[Object.keys(item).find(k => k === Object.keys(item)[0])]))]; | |
| if (distinctValues.length <= data.length * 0.2) return 'enum'; | |
| return 'string'; | |
| } | |
| return 'string'; | |
| } | |
| // Render table headers | |
| function renderTableHeaders() { | |
| tableHeader.innerHTML = ''; | |
| columnOrder.forEach((key, index) => { | |
| if (!columnConfig[key] || !columnConfig[key].visible) return; | |
| const th = document.createElement('th'); | |
| th.className = 'p-3 border-b border-gray-200 font-semibold text-gray-700 relative'; | |
| th.style.width = `${columnConfig[key].width}px`; | |
| th.dataset.column = key; | |
| // Column header content | |
| const headerContent = document.createElement('div'); | |
| headerContent.className = 'flex items-center justify-between'; | |
| const titleSpan = document.createElement('span'); | |
| titleSpan.className = 'draggable-header'; | |
| titleSpan.textContent = key; | |
| titleSpan.dataset.column = key; | |
| // Sort indicator | |
| const sortIcon = document.createElement('i'); | |
| sortIcon.className = 'fas fa-sort sort-icon ml-2 text-gray-400'; | |
| if (sortColumn === key) { | |
| sortIcon.classList.add(sortDirection === 'asc' ? 'fa-sort-up' : 'fa-sort-down'); | |
| sortIcon.classList.add('text-blue-500'); | |
| } | |
| // Column menu button | |
| const menuBtn = document.createElement('button'); | |
| menuBtn.className = 'ml-2 text-gray-400 hover:text-gray-600'; | |
| menuBtn.innerHTML = '<i class="fas fa-ellipsis-v"></i>'; | |
| menuBtn.onclick = (e) => { | |
| e.stopPropagation(); | |
| showColumnMenu(e.target.closest('th'), key); | |
| }; | |
| headerContent.appendChild(titleSpan); | |
| headerContent.appendChild(sortIcon); | |
| headerContent.appendChild(menuBtn); | |
| th.appendChild(headerContent); | |
| // Resize handle | |
| const resizeHandle = document.createElement('div'); | |
| resizeHandle.className = 'resize-handle'; | |
| th.appendChild(resizeHandle); | |
| // Add event listeners | |
| th.addEventListener('click', () => sortTable(key)); | |
| // Drag and drop for column reordering | |
| th.addEventListener('mousedown', (e) => { | |
| if (e.target.classList.contains('resize-handle')) { | |
| // Column resize | |
| isDragging = true; | |
| dragStartX = e.clientX; | |
| dragStartWidth = th.offsetWidth; | |
| dragColumnIndex = index; | |
| dragColumnElement = th; | |
| document.body.style.cursor = 'col-resize'; | |
| } else if (e.target.classList.contains('draggable-header') || e.target.closest('.draggable-header')) { | |
| // Column reorder | |
| isDraggingColumn = true; | |
| draggedColumnIndex = index; | |
| dragStartColumnX = e.clientX; | |
| th.style.opacity = '0.7'; | |
| document.body.style.cursor = 'move'; | |
| } | |
| }); | |
| tableHeader.appendChild(th); | |
| }); | |
| } | |
| // Show column menu | |
| function showColumnMenu(headerElement, columnKey) { | |
| // Close any existing menus | |
| document.querySelectorAll('.column-menu').forEach(el => el.remove()); | |
| const menu = document.createElement('div'); | |
| menu.className = 'column-menu absolute bg-white shadow-lg rounded-md py-1 z-20'; | |
| menu.style.top = `${headerElement.offsetTop + headerElement.offsetHeight}px`; | |
| menu.style.left = `${headerElement.offsetLeft}px`; | |
| const menuItems = [ | |
| { label: 'Hide Column', icon: 'fa-eye-slash', action: () => toggleColumnVisibility(columnKey, false) }, | |
| { label: 'Auto Fit Width', icon: 'fa-arrows-alt-h', action: () => autoFitColumn(columnKey) }, | |
| { label: 'Reset Width', icon: 'fa-undo', action: () => resetColumnWidth(columnKey) }, | |
| { label: 'Filter', icon: 'fa-filter', action: () => showFilterForColumn(columnKey) } | |
| ]; | |
| menuItems.forEach(item => { | |
| const menuItem = document.createElement('button'); | |
| menuItem.className = 'w-full text-left px-4 py-2 hover:bg-gray-100 flex items-center gap-2'; | |
| menuItem.innerHTML = `<i class="fas ${item.icon}"></i> ${item.label}`; | |
| menuItem.onclick = (e) => { | |
| e.stopPropagation(); | |
| item.action(); | |
| menu.remove(); | |
| }; | |
| menu.appendChild(menuItem); | |
| }); | |
| headerElement.appendChild(menu); | |
| // Close menu when clicking elsewhere | |
| setTimeout(() => { | |
| const closeMenu = (e) => { | |
| if (!headerElement.contains(e.target)) { | |
| menu.remove(); | |
| document.removeEventListener('click', closeMenu); | |
| } | |
| }; | |
| document.addEventListener('click', closeMenu); | |
| }, 0); | |
| } | |
| // Toggle column visibility | |
| function toggleColumnVisibility(columnKey, visible) { | |
| if (typeof visible === 'undefined') { | |
| columnConfig[columnKey].visible = !columnConfig[columnKey].visible; | |
| } else { | |
| columnConfig[columnKey].visible = visible; | |
| } | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| renderColumnOptions(); | |
| } | |
| // Auto fit column width | |
| function autoFitColumn(columnKey) { | |
| // Simple implementation - could be enhanced | |
| const maxContentWidth = Math.max( | |
| ...data.map(item => { | |
| const value = item[columnKey]; | |
| return measureTextWidth(value !== undefined && value !== null ? value.toString() : '', '14px Arial'); | |
| }), | |
| measureTextWidth(columnKey, '14px Arial') // Include header width | |
| ); | |
| columnConfig[columnKey].width = Math.min(Math.max(maxContentWidth + 40, 100), 500); // Min 100, max 500 | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| } | |
| // Reset column width | |
| function resetColumnWidth(columnKey) { | |
| columnConfig[columnKey].width = 200; | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| } | |
| // Show filter for specific column | |
| function showFilterForColumn(columnKey) { | |
| filterPanel.classList.remove('hidden'); | |
| toggleFiltersBtn.classList.add('bg-blue-500', 'text-white'); | |
| toggleFiltersBtn.classList.remove('bg-gray-200', 'text-gray-800'); | |
| // Scroll to the filter if it exists | |
| const existingFilter = document.querySelector(`.filter-control[data-column="${columnKey}"]`); | |
| if (existingFilter) { | |
| existingFilter.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| return; | |
| } | |
| // Otherwise add the filter | |
| addFilterControl(columnKey); | |
| } | |
| // Measure text width | |
| function measureTextWidth(text, font) { | |
| const canvas = document.createElement('canvas'); | |
| const context = canvas.getContext('2d'); | |
| context.font = font || '14px Arial'; | |
| return context.measureText(text).width; | |
| } | |
| // Render table body | |
| function renderTableBody() { | |
| tableBody.innerHTML = ''; | |
| if (filteredData.length === 0) { | |
| const tr = document.createElement('tr'); | |
| const td = document.createElement('td'); | |
| td.className = 'p-4 text-center text-gray-500'; | |
| td.colSpan = columnOrder.filter(key => columnConfig[key]?.visible).length; | |
| td.textContent = 'No data available'; | |
| tr.appendChild(td); | |
| tableBody.appendChild(tr); | |
| return; | |
| } | |
| const startIndex = (currentPage - 1) * rowsPerPage; | |
| const endIndex = Math.min(startIndex + rowsPerPage, filteredData.length); | |
| for (let i = startIndex; i < endIndex; i++) { | |
| const item = filteredData[i]; | |
| const tr = document.createElement('tr'); | |
| tr.className = i % 2 === 0 ? 'bg-white' : 'bg-gray-50'; | |
| columnOrder.forEach(key => { | |
| if (!columnConfig[key] || !columnConfig[key].visible) return; | |
| const td = document.createElement('td'); | |
| td.className = 'p-3 border-b border-gray-200 text-gray-700 relative'; | |
| // Handle long text with ellipsis | |
| const cellValue = item[key] !== undefined && item[key] !== null ? item[key].toString() : ''; | |
| const cellDiv = document.createElement('div'); | |
| cellDiv.className = 'ellipsis-text'; | |
| cellDiv.textContent = cellValue; | |
| cellDiv.title = cellValue; | |
| // Add click to view full text for long content | |
| if (cellValue.length > 50) { | |
| cellDiv.style.cursor = 'pointer'; | |
| cellDiv.onclick = () => showFullText(key, cellValue); | |
| } | |
| td.appendChild(cellDiv); | |
| tr.appendChild(td); | |
| }); | |
| tableBody.appendChild(tr); | |
| } | |
| } | |
| // Show full text in modal | |
| function showFullText(title, content) { | |
| modalTitle.textContent = title; | |
| modalContent.textContent = content; | |
| textModal.style.display = 'flex'; | |
| } | |
| // Render pagination | |
| function renderPagination() { | |
| const totalPages = Math.ceil(filteredData.length / rowsPerPage); | |
| pageInfo.textContent = `Page ${currentPage} of ${totalPages}`; | |
| firstPageBtn.disabled = currentPage === 1; | |
| prevPageBtn.disabled = currentPage === 1; | |
| nextPageBtn.disabled = currentPage === totalPages || totalPages === 0; | |
| lastPageBtn.disabled = currentPage === totalPages || totalPages === 0; | |
| } | |
| // Sort table | |
| function sortTable(columnKey) { | |
| if (sortColumn === columnKey) { | |
| sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; | |
| } else { | |
| sortColumn = columnKey; | |
| sortDirection = 'asc'; | |
| } | |
| filteredData.sort((a, b) => { | |
| const valA = a[columnKey]; | |
| const valB = b[columnKey]; | |
| if (valA === valB) return 0; | |
| if (valA === undefined || valA === null) return 1; | |
| if (valB === undefined || valB === null) return -1; | |
| if (typeof valA === 'number' && typeof valB === 'number') { | |
| return sortDirection === 'asc' ? valA - valB : valB - valA; | |
| } | |
| if (typeof valA === 'string' && typeof valB === 'string') { | |
| const strA = valA.toString().toLowerCase(); | |
| const strB = valB.toString().toLowerCase(); | |
| return sortDirection === 'asc' | |
| ? strA.localeCompare(strB) | |
| : strB.localeCompare(strA); | |
| } | |
| if (Date.parse(valA) && Date.parse(valB)) { | |
| const dateA = new Date(valA); | |
| const dateB = new Date(valB); | |
| return sortDirection === 'asc' | |
| ? dateA - dateB | |
| : dateB - dateA; | |
| } | |
| return 0; | |
| }); | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| } | |
| // Filter table | |
| function filterTable() { | |
| filteredData = data.filter(item => { | |
| return Object.entries(activeFilters).every(([columnKey, filter]) => { | |
| if (!filter || !filter.value) return true; | |
| const itemValue = item[columnKey]; | |
| if (itemValue === undefined || itemValue === null) return false; | |
| const strValue = itemValue.toString().toLowerCase(); | |
| const filterValue = filter.value.toString().toLowerCase(); | |
| switch (filter.type) { | |
| case 'string': | |
| return strValue.includes(filterValue); | |
| case 'number': | |
| if (isNaN(itemValue) || isNaN(filter.value)) return false; | |
| return filter.operator === '=' | |
| ? itemValue == filter.value | |
| : filter.operator === '<' | |
| ? itemValue < filter.value | |
| : itemValue > filter.value; | |
| case 'date': | |
| const itemDate = new Date(itemValue); | |
| const filterDate = new Date(filter.value); | |
| return filter.operator === '=' | |
| ? itemDate.getTime() === filterDate.getTime() | |
| : filter.operator === '<' | |
| ? itemDate < filterDate | |
| : itemDate > filterDate; | |
| case 'enum': | |
| return strValue === filterValue; | |
| default: | |
| return strValue.includes(filterValue); | |
| } | |
| }); | |
| }); | |
| // Apply global search if present | |
| if (globalSearchInput.value.trim()) { | |
| const searchTerm = globalSearchInput.value.trim().toLowerCase(); | |
| filteredData = filteredData.filter(item => { | |
| return Object.entries(item).some(([key, value]) => { | |
| if (!columnConfig[key]?.visible) return false; | |
| return value !== undefined && value !== null && | |
| value.toString().toLowerCase().includes(searchTerm); | |
| }); | |
| }); | |
| } | |
| // Reset to first page after filtering | |
| currentPage = 1; | |
| renderTableBody(); | |
| renderPagination(); | |
| } | |
| // Render column options | |
| function renderColumnOptions() { | |
| columnCheckboxes.innerHTML = ''; | |
| columnOrder.forEach(key => { | |
| const div = document.createElement('div'); | |
| div.className = 'flex items-center'; | |
| const checkbox = document.createElement('input'); | |
| checkbox.type = 'checkbox'; | |
| checkbox.id = `col-${key}`; | |
| checkbox.className = 'mr-2'; | |
| checkbox.checked = columnConfig[key]?.visible || false; | |
| checkbox.onchange = () => toggleColumnVisibility(key, checkbox.checked); | |
| const label = document.createElement('label'); | |
| label.htmlFor = `col-${key}`; | |
| label.textContent = key; | |
| label.className = 'text-sm'; | |
| div.appendChild(checkbox); | |
| div.appendChild(label); | |
| columnCheckboxes.appendChild(div); | |
| }); | |
| } | |
| // Render filter controls | |
| function renderFilterControls() { | |
| filterControls.innerHTML = ''; | |
| columnOrder.forEach(key => { | |
| if (!activeFilters[key]) return; | |
| const filter = activeFilters[key]; | |
| const div = document.createElement('div'); | |
| div.className = 'filter-control border-b border-gray-200 pb-4 mb-4'; | |
| div.dataset.column = key; | |
| const header = document.createElement('div'); | |
| header.className = 'flex justify-between items-center mb-2'; | |
| const title = document.createElement('h4'); | |
| title.className = 'font-medium'; | |
| title.textContent = key; | |
| const removeBtn = document.createElement('button'); | |
| removeBtn.className = 'text-red-500 hover:text-red-700'; | |
| removeBtn.innerHTML = '<i class="fas fa-times"></i>'; | |
| removeBtn.onclick = () => { | |
| delete activeFilters[key]; | |
| renderFilterControls(); | |
| filterTable(); | |
| }; | |
| header.appendChild(title); | |
| header.appendChild(removeBtn); | |
| div.appendChild(header); | |
| // Filter controls based on type | |
| if (filter.type === 'number' || filter.type === 'date') { | |
| const operatorSelect = document.createElement('select'); | |
| operatorSelect.className = 'border border-gray-300 rounded px-2 py-1 mr-2'; | |
| operatorSelect.value = filter.operator || '='; | |
| operatorSelect.onchange = (e) => { | |
| activeFilters[key].operator = e.target.value; | |
| }; | |
| ['=', '<', '>'].forEach(op => { | |
| const option = document.createElement('option'); | |
| option.value = op; | |
| option.textContent = op === '=' ? 'equals' : op === '<' ? 'less than' : 'greater than'; | |
| operatorSelect.appendChild(option); | |
| }); | |
| const valueInput = document.createElement('input'); | |
| valueInput.type = filter.type === 'date' ? 'date' : 'number'; | |
| valueInput.className = 'border border-gray-300 rounded px-2 py-1'; | |
| valueInput.value = filter.value || ''; | |
| valueInput.onchange = (e) => { | |
| activeFilters[key].value = e.target.value; | |
| }; | |
| div.appendChild(operatorSelect); | |
| div.appendChild(valueInput); | |
| } | |
| else if (filter.type === 'enum') { | |
| const distinctValues = [...new Set(data.map(item => item[key]))].sort(); | |
| const select = document.createElement('select'); | |
| select.className = 'w-full border border-gray-300 rounded px-2 py-1'; | |
| select.value = filter.value || ''; | |
| const emptyOption = document.createElement('option'); | |
| emptyOption.value = ''; | |
| emptyOption.textContent = 'Select a value'; | |
| select.appendChild(emptyOption); | |
| distinctValues.forEach(value => { | |
| const option = document.createElement('option'); | |
| option.value = value; | |
| option.textContent = value; | |
| select.appendChild(option); | |
| }); | |
| select.value = filter.value || ''; | |
| select.onchange = (e) => { | |
| activeFilters[key].value = e.target.value; | |
| }; | |
| div.appendChild(select); | |
| } | |
| else { // string or other types | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'w-full border border-gray-300 rounded px-2 py-1'; | |
| input.placeholder = `Filter by ${key}`; | |
| input.value = filter.value || ''; | |
| input.onchange = (e) => { | |
| activeFilters[key].value = e.target.value; | |
| }; | |
| div.appendChild(input); | |
| } | |
| filterControls.appendChild(div); | |
| }); | |
| } | |
| // Add filter control | |
| function addFilterControl(columnKey) { | |
| if (!columnConfig[columnKey]) return; | |
| // Initialize filter if it doesn't exist | |
| if (!activeFilters[columnKey]) { | |
| activeFilters[columnKey] = { | |
| type: columnConfig[columnKey].type, | |
| value: '', | |
| operator: '=' | |
| }; | |
| } | |
| renderFilterControls(); | |
| // Scroll to the new filter | |
| const newFilter = document.querySelector(`.filter-control[data-column="${columnKey}"]`); | |
| if (newFilter) { | |
| newFilter.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } | |
| } | |
| // Event listeners | |
| document.addEventListener('mousemove', (e) => { | |
| // Column resize | |
| if (isDragging && dragColumnElement) { | |
| const width = dragStartWidth + (e.clientX - dragStartX); | |
| const columnKey = dragColumnElement.dataset.column; | |
| columnConfig[columnKey].width = Math.max(width, 50); // Minimum width | |
| dragColumnElement.style.width = `${columnConfig[columnKey].width}px`; | |
| } | |
| // Column reordering | |
| if (isDraggingColumn) { | |
| // Visual feedback could be added here (like a placeholder or shadow) | |
| } | |
| }); | |
| document.addEventListener('mouseup', () => { | |
| if (isDragging) { | |
| isDragging = false; | |
| document.body.style.cursor = ''; | |
| if (dragColumnElement) { | |
| dragColumnElement.style.opacity = '1'; | |
| dragColumnElement = null; | |
| } | |
| } | |
| if (isDraggingColumn) { | |
| isDraggingColumn = false; | |
| document.body.style.cursor = ''; | |
| // Update column order if the drag position has changed significantly | |
| if (draggedColumnIndex >= 0) { | |
| const thElements = tableHeader.querySelectorAll('th'); | |
| thElements.forEach(th => th.style.opacity = '1'); | |
| // Determine the new position based on mouse position | |
| const targetIndex = calculateNewColumnPosition(draggedColumnIndex); | |
| if (targetIndex !== draggedColumnIndex) { | |
| // Update column order | |
| const columnKey = columnOrder[draggedColumnIndex]; | |
| columnOrder.splice(draggedColumnIndex, 1); | |
| columnOrder.splice(targetIndex, 0, columnKey); | |
| // Re-render table | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| } | |
| } | |
| } | |
| }); | |
| // Calculate new column position when dragging | |
| function calculateNewColumnPosition(currentIndex) { | |
| const thElements = tableHeader.querySelectorAll('th'); | |
| if (thElements.length === 0) return currentIndex; | |
| // Simple implementation - could be enhanced | |
| return currentIndex; // Placeholder | |
| } | |
| // Pagination controls | |
| firstPageBtn.addEventListener('click', () => { | |
| currentPage = 1; | |
| renderTableBody(); | |
| renderPagination(); | |
| }); | |
| prevPageBtn.addEventListener('click', () => { | |
| if (currentPage > 1) { | |
| currentPage--; | |
| renderTableBody(); | |
| renderPagination(); | |
| } | |
| }); | |
| nextPageBtn.addEventListener('click', () => { | |
| const totalPages = Math.ceil(filteredData.length / rowsPerPage); | |
| if (currentPage < totalPages) { | |
| currentPage++; | |
| renderTableBody(); | |
| renderPagination(); | |
| } | |
| }); | |
| lastPageBtn.addEventListener('click', () => { | |
| const totalPages = Math.ceil(filteredData.length / rowsPerPage); | |
| currentPage = totalPages; | |
| renderTableBody(); | |
| renderPagination(); | |
| }); | |
| rowsPerPageSelect.addEventListener('change', () => { | |
| rowsPerPage = parseInt(rowsPerPageSelect.value); | |
| currentPage = 1; | |
| renderTableBody(); | |
| renderPagination(); | |
| }); | |
| // Global search | |
| globalSearchInput.addEventListener('input', () => { | |
| currentPage = 1; | |
| filterTable(); | |
| }); | |
| // Toggle filters panel | |
| toggleFiltersBtn.addEventListener('click', () => { | |
| filterPanel.classList.toggle('hidden'); | |
| if (filterPanel.classList.contains('hidden')) { | |
| toggleFiltersBtn.classList.remove('bg-blue-500', 'text-white'); | |
| toggleFiltersBtn.classList.add('bg-gray-200', 'text-gray-800'); | |
| } else { | |
| toggleFiltersBtn.classList.add('bg-blue-500', 'text-white'); | |
| toggleFiltersBtn.classList.remove('bg-gray-200', 'text-gray-800'); | |
| columnOptionsPanel.classList.add('hidden'); | |
| } | |
| }); | |
| // Toggle columns panel | |
| toggleColumnsBtn.addEventListener('click', (e) => { | |
| columnOptionsPanel.classList.toggle('hidden'); | |
| if (columnOptionsPanel.classList.contains('hidden')) { | |
| toggleColumnsBtn.classList.remove('bg-blue-500', 'text-white'); | |
| toggleColumnsBtn.classList.add('bg-gray-200', 'text-gray-800'); | |
| } else { | |
| toggleColumnsBtn.classList.add('bg-blue-500', 'text-white'); | |
| toggleColumnsBtn.classList.remove('bg-gray-200', 'text-gray-800'); | |
| filterPanel.classList.add('hidden'); | |
| // Position the panel near the button | |
| columnOptionsPanel.style.right = '0'; | |
| columnOptionsPanel.style.left = 'auto'; | |
| } | |
| }); | |
| // Close filters | |
| closeFiltersBtn.addEventListener('click', () => { | |
| filterPanel.classList.add('hidden'); | |
| toggleFiltersBtn.classList.remove('bg-blue-500', 'text-white'); | |
| toggleFiltersBtn.classList.add('bg-gray-200', 'text-gray-800'); | |
| }); | |
| // Apply filters | |
| applyFiltersBtn.addEventListener('click', () => { | |
| filterTable(); | |
| }); | |
| // Reset filters | |
| resetFiltersBtn.addEventListener('click', () => { | |
| activeFilters = {}; | |
| globalSearchInput.value = ''; | |
| renderFilterControls(); | |
| filterTable(); | |
| }); | |
| // Reset columns | |
| resetColumnsBtn.addEventListener('click', () => { | |
| const firstItem = data[0]; | |
| columnOrder = Object.keys(firstItem); | |
| Object.keys(firstItem).forEach(key => { | |
| columnConfig[key] = { | |
| visible: true, | |
| width: 200, | |
| type: detectType(firstItem[key]) | |
| }; | |
| }); | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| renderColumnOptions(); | |
| renderFilterControls(); | |
| }); | |
| // File upload | |
| uploadBtn.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| try { | |
| data = JSON.parse(event.target.result); | |
| filteredData = [...data]; | |
| columnConfig = {}; | |
| columnOrder = []; | |
| activeFilters = {}; | |
| currentPage = 1; | |
| initTable(); | |
| } catch (error) { | |
| alert('Error parsing JSON file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| fileInput.value = ''; // Reset input | |
| }); | |
| // File drop area | |
| fileDropArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| fileDropArea.classList.add('active'); | |
| }); | |
| fileDropArea.addEventListener('dragleave', () => { | |
| fileDropArea.classList.remove('active'); | |
| }); | |
| fileDropArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| fileDropArea.classList.remove('active'); | |
| const file = e.dataTransfer.files[0]; | |
| if (!file) return; | |
| if (file.name.endsWith('.json')) { | |
| fileInput.files = e.dataTransfer.files; | |
| const event = new Event('change'); | |
| fileInput.dispatchEvent(event); | |
| } else { | |
| alert('Please upload a JSON file'); | |
| } | |
| }); | |
| fileDropArea.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| // Download data | |
| downloadBtn.addEventListener('click', () => { | |
| const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(filteredData, null, 2)); | |
| const downloadAnchorNode = document.createElement('a'); | |
| downloadAnchorNode.setAttribute('href', dataStr); | |
| downloadAnchorNode.setAttribute('download', 'table_data.json'); | |
| document.body.appendChild(downloadAnchorNode); | |
| downloadAnchorNode.click(); | |
| downloadAnchorNode.remove(); | |
| }); | |
| // Paste from clipboard | |
| pasteBtn.addEventListener('click', async () => { | |
| try { | |
| const text = await navigator.clipboard.readText(); | |
| data = JSON.parse(text); | |
| filteredData = [...data]; | |
| columnConfig = {}; | |
| columnOrder = []; | |
| activeFilters = {}; | |
| currentPage = 1; | |
| initTable(); | |
| } catch (error) { | |
| alert('Error pasting from clipboard: ' + error.message); | |
| } | |
| }); | |
| // Load from URL | |
| loadUrlBtn.addEventListener('click', async () => { | |
| const url = jsonUrl.value.trim(); | |
| if (!url) return; | |
| try { | |
| const response = await fetch(url); | |
| if (!response.ok) throw new Error('Failed to fetch data'); | |
| data = await response.json(); | |
| filteredData = [...data]; | |
| columnConfig = {}; | |
| columnOrder = []; | |
| activeFilters = {}; | |
| currentPage = 1; | |
| initTable(); | |
| } catch (error) { | |
| alert('Error loading from URL: ' + error.message); | |
| } | |
| }); | |
| // Save config | |
| saveConfigBtn.addEventListener('click', () => { | |
| const config = { | |
| columnConfig, | |
| columnOrder, | |
| sortColumn, | |
| sortDirection | |
| }; | |
| const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, 2)); | |
| const downloadAnchorNode = document.createElement('a'); | |
| downloadAnchorNode.setAttribute('href', dataStr); | |
| downloadAnchorNode.setAttribute('download', 'table_config.json'); | |
| document.body.appendChild(downloadAnchorNode); | |
| downloadAnchorNode.click(); | |
| downloadAnchorNode.remove(); | |
| }); | |
| // Load config | |
| loadConfigBtn.addEventListener('click', () => { | |
| const input = document.createElement('input'); | |
| input.type = 'file'; | |
| input.accept = '.json'; | |
| input.onchange = e => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = event => { | |
| try { | |
| const config = JSON.parse(event.target.result); | |
| if (config.columnConfig) columnConfig = config.columnConfig; | |
| if (config.columnOrder) columnOrder = config.columnOrder; | |
| if (config.sortColumn) sortColumn = config.sortColumn; | |
| if (config.sortDirection) sortDirection = config.sortDirection; | |
| // Make sure all current columns are included in config | |
| if (data.length > 0) { | |
| const firstItem = data[0]; | |
| Object.keys(firstItem).forEach(key => { | |
| if (!columnConfig[key]) { | |
| columnConfig[key] = { | |
| visible: true, | |
| width: 200, | |
| type: detectType(firstItem[key]) | |
| }; | |
| } | |
| if (!columnOrder.includes(key)) { | |
| columnOrder.push(key); | |
| } | |
| }); | |
| } | |
| renderTableHeaders(); | |
| renderTableBody(); | |
| renderColumnOptions(); | |
| renderFilterControls(); | |
| renderPagination(); | |
| } catch (error) { | |
| alert('Error parsing config file: ' + error.message); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| }; | |
| input.click(); | |
| }); | |
| // Close modal | |
| closeModalBtn.addEventListener('click', () => { | |
| textModal.style.display = 'none'; | |
| }); | |
| // Click outside modal to close | |
| textModal.addEventListener('click', (e) => { | |
| if (e.target === textModal) { | |
| textModal.style.display = 'none'; | |
| } | |
| }); | |
| // Initialize the table on load | |
| window.addEventListener('load', initTable); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=weisanju/frontend" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p><p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Yoleo/tabla-json" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |