Created
February 22, 2025 06:29
-
-
Save rcanand/cc0a7dc87839ac1c23a0e687c779637c to your computer and use it in GitHub Desktop.
Standalone html file to view local csvs in a table, sort and filter columns.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>CSV Viewer</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script> | |
<style> | |
:root { | |
--bg-primary: #1a1a1a; | |
--bg-secondary: #2d2d2d; | |
--text-primary: #ffffff; | |
--text-secondary: #b3b3b3; | |
--accent: #4a9eff; | |
--border: #404040; | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, | |
sans-serif; | |
background-color: var(--bg-primary); | |
color: var(--text-primary); | |
display: grid; | |
grid-template-columns: 250px 1fr; | |
min-height: 100vh; | |
} | |
.sidebar { | |
background-color: var(--bg-secondary); | |
padding: 1rem; | |
border-right: 1px solid var(--border); | |
} | |
.file-input-container { | |
margin-bottom: 1rem; | |
} | |
.file-input-label { | |
display: block; | |
background-color: var(--accent); | |
color: var(--text-primary); | |
padding: 0.5rem 1rem; | |
border-radius: 4px; | |
cursor: pointer; | |
text-align: center; | |
} | |
.file-input { | |
display: none; | |
} | |
.file-list { | |
list-style: none; | |
} | |
.file-list li { | |
padding: 0.5rem; | |
cursor: pointer; | |
border-radius: 4px; | |
margin-bottom: 0.25rem; | |
} | |
.file-list li:hover { | |
background-color: var(--bg-primary); | |
} | |
.file-list li.active { | |
background-color: var(--accent); | |
} | |
.main-content { | |
padding: 1rem; | |
overflow: auto; | |
} | |
.table-container { | |
width: 100%; | |
overflow-x: auto; | |
} | |
.filter-row { | |
display: flex; | |
gap: 0.5rem; | |
margin-bottom: 1rem; | |
} | |
.filter-input { | |
background-color: var(--bg-secondary); | |
border: 1px solid var(--border); | |
color: var(--text-primary); | |
padding: 0.25rem 0.5rem; | |
border-radius: 4px; | |
flex: 1; | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
margin-top: 1rem; | |
} | |
th, | |
td { | |
padding: 0.5rem; | |
text-align: left; | |
border: 1px solid var(--border); | |
} | |
th { | |
background-color: var(--bg-secondary); | |
cursor: pointer; | |
user-select: none; | |
} | |
th:hover { | |
background-color: var(--accent); | |
} | |
tr:nth-child(even) { | |
background-color: var(--bg-secondary); | |
} | |
</style> | |
</head> | |
<body> | |
<div class="sidebar"> | |
<div class="file-input-container"> | |
<label class="file-input-label"> | |
Add CSV Files | |
<input type="file" class="file-input" accept=".csv" multiple /> | |
</label> | |
</div> | |
<ul class="file-list"></ul> | |
</div> | |
<div class="main-content"> | |
<div id="table-container" class="table-container"></div> | |
</div> | |
<script> | |
class CSVViewer { | |
constructor() { | |
this.files = new Map(); | |
this.currentFile = null; | |
this.sortColumn = null; | |
this.sortAscending = true; | |
this.fileInput = document.querySelector(".file-input"); | |
this.fileList = document.querySelector(".file-list"); | |
this.tableContainer = document.querySelector("#table-container"); | |
this.initEventListeners(); | |
} | |
initEventListeners() { | |
this.fileInput.addEventListener("change", (e) => | |
this.handleFileSelect(e) | |
); | |
} | |
async handleFileSelect(event) { | |
const files = event.target.files; | |
for (const file of files) { | |
Papa.parse(file, { | |
header: true, | |
complete: (results) => { | |
this.files.set(file.name, { | |
headers: Object.keys(results.data[0]), | |
rows: results.data.map(Object.values), | |
}); | |
this.addFileToList(file.name); | |
if (this.files.size === 1) { | |
this.showFile(file.name); | |
} | |
}, | |
}); | |
} | |
} | |
addFileToList(fileName) { | |
const li = document.createElement("li"); | |
li.textContent = fileName; | |
li.addEventListener("click", () => this.showFile(fileName)); | |
this.fileList.appendChild(li); | |
} | |
showFile(fileName) { | |
this.currentFile = fileName; | |
const { headers, rows } = this.files.get(fileName); | |
// Update active state in file list | |
this.fileList.querySelectorAll("li").forEach((li) => { | |
li.classList.toggle("active", li.textContent === fileName); | |
}); | |
// Create filter inputs | |
const filterRow = document.createElement("div"); | |
filterRow.className = "filter-row"; | |
headers.forEach((header, index) => { | |
const input = document.createElement("input"); | |
input.type = "text"; | |
input.placeholder = `Filter ${header}`; | |
input.className = "filter-input"; | |
input.addEventListener("input", () => this.filterTable()); | |
filterRow.appendChild(input); | |
}); | |
// Create table | |
const table = document.createElement("table"); | |
const thead = document.createElement("thead"); | |
const headerRow = document.createElement("tr"); | |
headers.forEach((header, index) => { | |
const th = document.createElement("th"); | |
th.textContent = header; | |
th.addEventListener("click", () => this.sortTable(index)); | |
headerRow.appendChild(th); | |
}); | |
thead.appendChild(headerRow); | |
table.appendChild(thead); | |
const tbody = document.createElement("tbody"); | |
rows.forEach((row) => { | |
const tr = document.createElement("tr"); | |
row.forEach((cell) => { | |
const td = document.createElement("td"); | |
td.textContent = cell; | |
tr.appendChild(td); | |
}); | |
tbody.appendChild(tr); | |
}); | |
table.appendChild(tbody); | |
this.tableContainer.innerHTML = ""; | |
this.tableContainer.appendChild(filterRow); | |
this.tableContainer.appendChild(table); | |
} | |
filterTable() { | |
const filters = Array.from( | |
document.querySelectorAll(".filter-input") | |
).map((input) => input.value.toLowerCase()); | |
const tbody = document.querySelector("tbody"); | |
const rows = Array.from(tbody.querySelectorAll("tr")); | |
rows.forEach((row) => { | |
const cells = Array.from(row.querySelectorAll("td")); | |
const visible = cells.every((cell, index) => { | |
return cell.textContent.toLowerCase().includes(filters[index]); | |
}); | |
row.style.display = visible ? "" : "none"; | |
}); | |
} | |
sortTable(columnIndex) { | |
const { headers, rows } = this.files.get(this.currentFile); | |
// Toggle sort direction if clicking the same column | |
if (this.sortColumn === columnIndex) { | |
this.sortAscending = !this.sortAscending; | |
} else { | |
this.sortColumn = columnIndex; | |
this.sortAscending = true; | |
} | |
// Sort the rows | |
rows.sort((a, b) => { | |
let valueA = a[columnIndex]; | |
let valueB = b[columnIndex]; | |
// Try to convert to numbers if possible | |
const numA = Number(valueA); | |
const numB = Number(valueB); | |
if (!isNaN(numA) && !isNaN(numB)) { | |
valueA = numA; | |
valueB = numB; | |
} | |
if (valueA < valueB) return this.sortAscending ? -1 : 1; | |
if (valueA > valueB) return this.sortAscending ? 1 : -1; | |
return 0; | |
}); | |
// Update the display | |
this.showFile(this.currentFile); | |
} | |
} | |
new CSVViewer(); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment