Skip to content

Instantly share code, notes, and snippets.

@rcanand
Created February 22, 2025 06:29
Show Gist options
  • Save rcanand/cc0a7dc87839ac1c23a0e687c779637c to your computer and use it in GitHub Desktop.
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.
<!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