|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>MultiView Data Grid</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> |
|
|
.card-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
|
|
gap: 1.5rem; |
|
|
} |
|
|
|
|
|
.data-table { |
|
|
width: 100%; |
|
|
border-collapse: collapse; |
|
|
} |
|
|
|
|
|
.data-table th, .data-table td { |
|
|
padding: 0.75rem 1rem; |
|
|
text-align: left; |
|
|
border-bottom: 1px solid #e5e7eb; |
|
|
} |
|
|
|
|
|
.data-table th { |
|
|
background-color: #f3f4f6; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.data-table tr:hover { |
|
|
background-color: #f9fafb; |
|
|
} |
|
|
|
|
|
.pagination { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
gap: 0.5rem; |
|
|
margin-top: 1.5rem; |
|
|
} |
|
|
|
|
|
.pagination button { |
|
|
padding: 0.5rem 1rem; |
|
|
border: 1px solid #e5e7eb; |
|
|
border-radius: 0.375rem; |
|
|
background-color: white; |
|
|
} |
|
|
|
|
|
.pagination button.active { |
|
|
background-color: #3b82f6; |
|
|
color: white; |
|
|
border-color: #3b82f6; |
|
|
} |
|
|
|
|
|
.search-filter { |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.search-filter:focus { |
|
|
outline: none; |
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3); |
|
|
} |
|
|
|
|
|
.toggle-switch { |
|
|
position: relative; |
|
|
display: inline-block; |
|
|
width: 60px; |
|
|
height: 30px; |
|
|
} |
|
|
|
|
|
.toggle-switch input { |
|
|
opacity: 0; |
|
|
width: 0; |
|
|
height: 0; |
|
|
} |
|
|
|
|
|
.slider { |
|
|
position: absolute; |
|
|
cursor: pointer; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background-color: #ccc; |
|
|
transition: .4s; |
|
|
border-radius: 34px; |
|
|
} |
|
|
|
|
|
.slider:before { |
|
|
position: absolute; |
|
|
content: ""; |
|
|
height: 22px; |
|
|
width: 22px; |
|
|
left: 4px; |
|
|
bottom: 4px; |
|
|
background-color: white; |
|
|
transition: .4s; |
|
|
border-radius: 50%; |
|
|
} |
|
|
|
|
|
input:checked + .slider { |
|
|
background-color: #3b82f6; |
|
|
} |
|
|
|
|
|
input:checked + .slider:before { |
|
|
transform: translateX(30px); |
|
|
} |
|
|
|
|
|
.toggle-icons { |
|
|
position: absolute; |
|
|
top: 50%; |
|
|
transform: translateY(-50%); |
|
|
color: white; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
.table-icon { |
|
|
left: 8px; |
|
|
} |
|
|
|
|
|
.grid-icon { |
|
|
right: 8px; |
|
|
} |
|
|
|
|
|
input:checked + .slider .table-icon { |
|
|
opacity: 0; |
|
|
} |
|
|
|
|
|
input:not(:checked) + .slider .grid-icon { |
|
|
opacity: 0; |
|
|
} |
|
|
|
|
|
@media (max-width: 640px) { |
|
|
.card-grid { |
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
|
|
} |
|
|
|
|
|
.data-table th, .data-table td { |
|
|
padding: 0.5rem; |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
|
|
|
.controls-container { |
|
|
flex-direction: column; |
|
|
gap: 1rem; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body class="bg-gray-50 min-h-screen"> |
|
|
<div class="container mx-auto px-4 py-8 max-w-7xl"> |
|
|
<div class="bg-white rounded-lg shadow-md p-6"> |
|
|
<h1 class="text-2xl font-bold text-gray-800 mb-6">MultiView Data Grid</h1> |
|
|
|
|
|
<div id="multiViewGrid" class="space-y-6"> |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
|
|
const sampleData = [ |
|
|
{ id: 1, name: 'John Doe', email: 'john@example.com', age: 28, status: 'Active', department: 'Engineering' }, |
|
|
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', age: 32, status: 'Active', department: 'Marketing' }, |
|
|
{ id: 3, name: 'Robert Johnson', email: 'robert@example.com', age: 45, status: 'Inactive', department: 'Sales' }, |
|
|
{ id: 4, name: 'Emily Davis', email: 'emily@example.com', age: 24, status: 'Active', department: 'Engineering' }, |
|
|
{ id: 5, name: 'Michael Brown', email: 'michael@example.com', age: 38, status: 'Active', department: 'HR' }, |
|
|
{ id: 6, name: 'Sarah Wilson', email: 'sarah@example.com', age: 29, status: 'Inactive', department: 'Marketing' }, |
|
|
{ id: 7, name: 'David Taylor', email: 'david@example.com', age: 41, status: 'Active', department: 'Engineering' }, |
|
|
{ id: 8, name: 'Lisa Anderson', email: 'lisa@example.com', age: 35, status: 'Active', department: 'Finance' }, |
|
|
{ id: 9, name: 'James Martinez', email: 'james@example.com', age: 27, status: 'Inactive', department: 'Sales' }, |
|
|
{ id: 10, name: 'Jennifer Thomas', email: 'jennifer@example.com', age: 31, status: 'Active', department: 'Marketing' }, |
|
|
{ id: 11, name: 'Christopher Lee', email: 'chris@example.com', age: 40, status: 'Active', department: 'Engineering' }, |
|
|
{ id: 12, name: 'Amanda White', email: 'amanda@example.com', age: 26, status: 'Inactive', department: 'HR' } |
|
|
]; |
|
|
|
|
|
|
|
|
const columns = [ |
|
|
{ field: 'id', headerName: 'ID', width: 70 }, |
|
|
{ field: 'name', headerName: 'Name', width: 150 }, |
|
|
{ field: 'email', headerName: 'Email', width: 200 }, |
|
|
{ field: 'age', headerName: 'Age', width: 90 }, |
|
|
{ field: 'department', headerName: 'Department', width: 150 }, |
|
|
{ field: 'status', headerName: 'Status', width: 120 } |
|
|
]; |
|
|
|
|
|
|
|
|
function cardRenderer(item) { |
|
|
return ` |
|
|
<div class="bg-white rounded-lg shadow-md overflow-hidden border border-gray-200 hover:shadow-lg transition-shadow duration-300"> |
|
|
<div class="p-4"> |
|
|
<div class="flex items-center justify-between mb-3"> |
|
|
<h3 class="text-lg font-semibold text-gray-800">${item.name}</h3> |
|
|
<span class="px-2 py-1 text-xs rounded-full ${item.status === 'Active' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}"> |
|
|
${item.status} |
|
|
</span> |
|
|
</div> |
|
|
<div class="space-y-2 text-sm text-gray-600"> |
|
|
<p><i class="fas fa-envelope mr-2"></i> ${item.email}</p> |
|
|
<p><i class="fas fa-id-card mr-2"></i> ID: ${item.id}</p> |
|
|
<p><i class="fas fa-birthday-cake mr-2"></i> Age: ${item.age}</p> |
|
|
<p><i class="fas fa-building mr-2"></i> ${item.department}</p> |
|
|
</div> |
|
|
</div> |
|
|
<div class="bg-gray-50 px-4 py-3 flex justify-end"> |
|
|
<button class="text-blue-600 hover:text-blue-800 text-sm font-medium"> |
|
|
View Details |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
const multiViewGrid = new MultiViewDataGrid({ |
|
|
container: document.getElementById('multiViewGrid'), |
|
|
data: sampleData, |
|
|
columns: columns, |
|
|
cardRenderer: cardRenderer, |
|
|
defaultView: 'table', |
|
|
itemsPerPage: 6 |
|
|
}); |
|
|
|
|
|
multiViewGrid.render(); |
|
|
}); |
|
|
|
|
|
class MultiViewDataGrid { |
|
|
constructor(options) { |
|
|
this.container = options.container; |
|
|
this.data = options.data || []; |
|
|
this.columns = options.columns || []; |
|
|
this.cardRenderer = options.cardRenderer || ((item) => `<div>${JSON.stringify(item)}</div>`); |
|
|
this.currentView = options.defaultView || 'table'; |
|
|
this.itemsPerPage = options.itemsPerPage || 10; |
|
|
this.currentPage = 1; |
|
|
this.searchTerm = ''; |
|
|
this.sortConfig = { field: null, direction: 'asc' }; |
|
|
this.filteredData = [...this.data]; |
|
|
} |
|
|
|
|
|
render() { |
|
|
|
|
|
this.container.innerHTML = ''; |
|
|
|
|
|
|
|
|
this.renderControls(); |
|
|
|
|
|
|
|
|
if (this.currentView === 'table') { |
|
|
this.renderTableView(); |
|
|
} else { |
|
|
this.renderGridView(); |
|
|
} |
|
|
|
|
|
|
|
|
this.renderPagination(); |
|
|
} |
|
|
|
|
|
renderControls() { |
|
|
const controlsContainer = document.createElement('div'); |
|
|
controlsContainer.className = 'controls-container flex flex-wrap items-center justify-between gap-4 mb-6'; |
|
|
|
|
|
|
|
|
const searchContainer = document.createElement('div'); |
|
|
searchContainer.className = 'relative flex-1 min-w-[200px]'; |
|
|
|
|
|
const searchInput = document.createElement('input'); |
|
|
searchInput.type = 'text'; |
|
|
searchInput.placeholder = 'Search...'; |
|
|
searchInput.className = 'search-filter w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:border-blue-500'; |
|
|
searchInput.value = this.searchTerm; |
|
|
searchInput.addEventListener('input', (e) => { |
|
|
this.searchTerm = e.target.value.toLowerCase(); |
|
|
this.currentPage = 1; |
|
|
this.filterData(); |
|
|
this.render(); |
|
|
}); |
|
|
|
|
|
const searchIcon = document.createElement('i'); |
|
|
searchIcon.className = 'fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400'; |
|
|
|
|
|
searchContainer.appendChild(searchIcon); |
|
|
searchContainer.appendChild(searchInput); |
|
|
|
|
|
|
|
|
const toggleContainer = document.createElement('div'); |
|
|
toggleContainer.className = 'flex items-center gap-3'; |
|
|
|
|
|
const toggleLabel = document.createElement('span'); |
|
|
toggleLabel.className = 'text-sm font-medium text-gray-700'; |
|
|
toggleLabel.textContent = 'Toggle View:'; |
|
|
|
|
|
const toggleWrapper = document.createElement('label'); |
|
|
toggleWrapper.className = 'toggle-switch'; |
|
|
|
|
|
const toggleInput = document.createElement('input'); |
|
|
toggleInput.type = 'checkbox'; |
|
|
toggleInput.checked = this.currentView === 'grid'; |
|
|
toggleInput.addEventListener('change', () => { |
|
|
this.currentView = toggleInput.checked ? 'grid' : 'table'; |
|
|
this.render(); |
|
|
}); |
|
|
|
|
|
const toggleSlider = document.createElement('span'); |
|
|
toggleSlider.className = 'slider'; |
|
|
|
|
|
const tableIcon = document.createElement('i'); |
|
|
tableIcon.className = 'toggle-icons table-icon fas fa-table'; |
|
|
|
|
|
const gridIcon = document.createElement('i'); |
|
|
gridIcon.className = 'toggle-icons grid-icon fas fa-th'; |
|
|
|
|
|
toggleSlider.appendChild(tableIcon); |
|
|
toggleSlider.appendChild(gridIcon); |
|
|
toggleWrapper.appendChild(toggleInput); |
|
|
toggleWrapper.appendChild(toggleSlider); |
|
|
|
|
|
toggleContainer.appendChild(toggleLabel); |
|
|
toggleContainer.appendChild(toggleWrapper); |
|
|
|
|
|
|
|
|
if (this.currentView === 'table') { |
|
|
const columnSelectContainer = document.createElement('div'); |
|
|
columnSelectContainer.className = 'w-full md:w-auto'; |
|
|
|
|
|
const columnSelectLabel = document.createElement('label'); |
|
|
columnSelectLabel.className = 'block text-sm font-medium text-gray-700 mb-1'; |
|
|
columnSelectLabel.textContent = 'Columns:'; |
|
|
|
|
|
const columnSelect = document.createElement('select'); |
|
|
columnSelect.className = 'block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md'; |
|
|
columnSelect.multiple = true; |
|
|
|
|
|
|
|
|
this.columns.forEach(col => { |
|
|
const option = document.createElement('option'); |
|
|
option.value = col.field; |
|
|
option.textContent = col.headerName; |
|
|
option.selected = true; |
|
|
columnSelect.appendChild(option); |
|
|
}); |
|
|
|
|
|
columnSelectContainer.appendChild(columnSelectLabel); |
|
|
columnSelectContainer.appendChild(columnSelect); |
|
|
controlsContainer.appendChild(columnSelectContainer); |
|
|
} |
|
|
|
|
|
controlsContainer.appendChild(searchContainer); |
|
|
controlsContainer.appendChild(toggleContainer); |
|
|
|
|
|
this.container.appendChild(controlsContainer); |
|
|
} |
|
|
|
|
|
renderTableView() { |
|
|
const tableContainer = document.createElement('div'); |
|
|
tableContainer.className = 'overflow-x-auto'; |
|
|
|
|
|
const table = document.createElement('table'); |
|
|
table.className = 'data-table w-full'; |
|
|
|
|
|
|
|
|
const thead = document.createElement('thead'); |
|
|
const headerRow = document.createElement('tr'); |
|
|
|
|
|
this.columns.forEach(col => { |
|
|
const th = document.createElement('th'); |
|
|
th.textContent = col.headerName; |
|
|
th.className = 'cursor-pointer hover:bg-gray-100'; |
|
|
th.addEventListener('click', () => { |
|
|
if (this.sortConfig.field === col.field) { |
|
|
this.sortConfig.direction = this.sortConfig.direction === 'asc' ? 'desc' : 'asc'; |
|
|
} else { |
|
|
this.sortConfig.field = col.field; |
|
|
this.sortConfig.direction = 'asc'; |
|
|
} |
|
|
this.filterData(); |
|
|
this.render(); |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.sortConfig.field === col.field) { |
|
|
const sortIcon = document.createElement('i'); |
|
|
sortIcon.className = `fas fa-sort-${this.sortConfig.direction === 'asc' ? 'up' : 'down'} ml-1`; |
|
|
th.appendChild(sortIcon); |
|
|
} |
|
|
|
|
|
headerRow.appendChild(th); |
|
|
}); |
|
|
|
|
|
thead.appendChild(headerRow); |
|
|
table.appendChild(thead); |
|
|
|
|
|
|
|
|
const tbody = document.createElement('tbody'); |
|
|
|
|
|
const paginatedData = this.getPaginatedData(); |
|
|
paginatedData.forEach(item => { |
|
|
const row = document.createElement('tr'); |
|
|
row.className = 'hover:bg-gray-50'; |
|
|
|
|
|
this.columns.forEach(col => { |
|
|
const td = document.createElement('td'); |
|
|
td.textContent = item[col.field]; |
|
|
row.appendChild(td); |
|
|
}); |
|
|
|
|
|
tbody.appendChild(row); |
|
|
}); |
|
|
|
|
|
table.appendChild(tbody); |
|
|
tableContainer.appendChild(table); |
|
|
this.container.appendChild(tableContainer); |
|
|
} |
|
|
|
|
|
renderGridView() { |
|
|
const gridContainer = document.createElement('div'); |
|
|
gridContainer.className = 'card-grid'; |
|
|
|
|
|
const paginatedData = this.getPaginatedData(); |
|
|
paginatedData.forEach(item => { |
|
|
const card = document.createElement('div'); |
|
|
card.innerHTML = this.cardRenderer(item); |
|
|
gridContainer.appendChild(card); |
|
|
}); |
|
|
|
|
|
this.container.appendChild(gridContainer); |
|
|
} |
|
|
|
|
|
renderPagination() { |
|
|
const totalPages = Math.ceil(this.filteredData.length / this.itemsPerPage); |
|
|
if (totalPages <= 1) return; |
|
|
|
|
|
const paginationContainer = document.createElement('div'); |
|
|
paginationContainer.className = 'pagination'; |
|
|
|
|
|
|
|
|
const prevButton = document.createElement('button'); |
|
|
prevButton.innerHTML = '<i class="fas fa-chevron-left"></i>'; |
|
|
prevButton.disabled = this.currentPage === 1; |
|
|
prevButton.addEventListener('click', () => { |
|
|
if (this.currentPage > 1) { |
|
|
this.currentPage--; |
|
|
this.render(); |
|
|
} |
|
|
}); |
|
|
paginationContainer.appendChild(prevButton); |
|
|
|
|
|
|
|
|
const maxVisiblePages = 5; |
|
|
let startPage = Math.max(1, this.currentPage - Math.floor(maxVisiblePages / 2)); |
|
|
let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1); |
|
|
|
|
|
if (endPage - startPage + 1 < maxVisiblePages) { |
|
|
startPage = Math.max(1, endPage - maxVisiblePages + 1); |
|
|
} |
|
|
|
|
|
if (startPage > 1) { |
|
|
const firstPageButton = document.createElement('button'); |
|
|
firstPageButton.textContent = '1'; |
|
|
firstPageButton.addEventListener('click', () => { |
|
|
this.currentPage = 1; |
|
|
this.render(); |
|
|
}); |
|
|
paginationContainer.appendChild(firstPageButton); |
|
|
|
|
|
if (startPage > 2) { |
|
|
const ellipsis = document.createElement('span'); |
|
|
ellipsis.textContent = '...'; |
|
|
ellipsis.className = 'flex items-center'; |
|
|
paginationContainer.appendChild(ellipsis); |
|
|
} |
|
|
} |
|
|
|
|
|
for (let i = startPage; i <= endPage; i++) { |
|
|
const pageButton = document.createElement('button'); |
|
|
pageButton.textContent = i; |
|
|
if (i === this.currentPage) { |
|
|
pageButton.className = 'active'; |
|
|
} |
|
|
pageButton.addEventListener('click', () => { |
|
|
this.currentPage = i; |
|
|
this.render(); |
|
|
}); |
|
|
paginationContainer.appendChild(pageButton); |
|
|
} |
|
|
|
|
|
if (endPage < totalPages) { |
|
|
if (endPage < totalPages - 1) { |
|
|
const ellipsis = document.createElement('span'); |
|
|
ellipsis.textContent = '...'; |
|
|
ellipsis.className = 'flex items-center'; |
|
|
paginationContainer.appendChild(ellipsis); |
|
|
} |
|
|
|
|
|
const lastPageButton = document.createElement('button'); |
|
|
lastPageButton.textContent = totalPages; |
|
|
lastPageButton.addEventListener('click', () => { |
|
|
this.currentPage = totalPages; |
|
|
this.render(); |
|
|
}); |
|
|
paginationContainer.appendChild(lastPageButton); |
|
|
} |
|
|
|
|
|
|
|
|
const nextButton = document.createElement('button'); |
|
|
nextButton.innerHTML = '<i class="fas fa-chevron-right"></i>'; |
|
|
nextButton.disabled = this.currentPage === totalPages; |
|
|
nextButton.addEventListener('click', () => { |
|
|
if (this.currentPage < totalPages) { |
|
|
this.currentPage++; |
|
|
this.render(); |
|
|
} |
|
|
}); |
|
|
paginationContainer.appendChild(nextButton); |
|
|
|
|
|
this.container.appendChild(paginationContainer); |
|
|
} |
|
|
|
|
|
filterData() { |
|
|
|
|
|
let filtered = this.data; |
|
|
if (this.searchTerm) { |
|
|
filtered = this.data.filter(item => { |
|
|
return Object.values(item).some( |
|
|
val => val.toString().toLowerCase().includes(this.searchTerm) |
|
|
); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.sortConfig.field) { |
|
|
filtered.sort((a, b) => { |
|
|
const aValue = a[this.sortConfig.field]; |
|
|
const bValue = b[this.sortConfig.field]; |
|
|
|
|
|
if (aValue < bValue) { |
|
|
return this.sortConfig.direction === 'asc' ? -1 : 1; |
|
|
} |
|
|
if (aValue > bValue) { |
|
|
return this.sortConfig.direction === 'asc' ? 1 : -1; |
|
|
} |
|
|
return 0; |
|
|
}); |
|
|
} |
|
|
|
|
|
this.filteredData = filtered; |
|
|
} |
|
|
|
|
|
getPaginatedData() { |
|
|
const startIndex = (this.currentPage - 1) * this.itemsPerPage; |
|
|
const endIndex = startIndex + this.itemsPerPage; |
|
|
return this.filteredData.slice(startIndex, endIndex); |
|
|
} |
|
|
} |
|
|
</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=MVCavalheiroJr/poc-multiview-data-grid" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
|
</html> |