Skip to content

Instantly share code, notes, and snippets.

@zbalkan
Last active March 24, 2025 15:56
Show Gist options
  • Save zbalkan/0429a22ece1d88ab5df5fa57cce0c859 to your computer and use it in GitHub Desktop.
Save zbalkan/0429a22ece1d88ab5df5fa57cce0c859 to your computer and use it in GitHub Desktop.
Single-page CSV viewer with search and sort options. It can be used for basic report generation over scripts.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CSV Table Viewer</title>
<style>
body {
background-color: #3e94ec;
font: 18px/28px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
font-size: 16px;
font-weight: 400;
text-rendering: optimizeLegibility;
margin: 0;
padding: 0;
}
.wrapper {
width: 80%;
margin: 0 auto;
}
header {
padding: 30px 0;
text-align: center;
font-size: 28px;
font-weight: bold;
color: #fafafa;
text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
}
footer {
margin-top: 40px;
padding: 20px 0;
text-align: center;
font-size: 14px;
color: #fafafa;
border-top: 1px solid rgba(255,255,255,0.3);
}
.searchBox {
margin: 30px 0;
font: inherit;
padding: 5px 15px;
width: 100%;
max-width: 400px;
display: block;
border: 1px solid #ccc;
border-radius: 4px;
}
/* Table container title (optional) */
.table-title {
text-align: center;
margin-bottom: 10px;
}
.table-fill {
background: white;
border-radius: 3px;
border-collapse: collapse;
margin: auto;
width: 100%;
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
animation: float 5s infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-2px); }
}
th {
color: #D5DDE5;
background: #1b1e24;
border-bottom: 4px solid #9ea7af;
border-right: 1px solid #343a45;
font-size: 20px;
font-weight: 100;
padding: 20px;
text-align: left;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
vertical-align: middle;
position: relative;
cursor: pointer;
}
th:first-child {
border-top-left-radius: 3px;
}
th:last-child {
border-top-right-radius: 3px;
border-right: none;
}
.arrow_box {
width: 10px;
height: 10px;
position: absolute;
display: inline;
top: 25px;
padding-left: 25px;
}
.arrow_box:after,
.arrow_box:before {
border: solid transparent;
content: " ";
position: absolute;
}
.arrow_box:before {
border-top-color: #fff;
opacity: 0.2;
border-width: 5px;
}
.arrow_box:after {
border-bottom-color: #fff;
opacity: 0.2;
border-width: 5px;
position: absolute;
top: -12px;
}
.descending .arrow_box:before {
opacity: 1;
}
.descending .arrow_box:after {
opacity: 0;
}
.ascending .arrow_box:before {
opacity: 0;
}
.ascending .arrow_box:after {
opacity: 1;
}
tr {
border-top: 1px solid #C1C3D1;
color: #666B85;
font-size: 16px;
font-weight: normal;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.1);
}
tr:hover td {
background: #4E5066;
color: #FFFFFF;
border-top: 1px solid #22262e;
}
tr:nth-child(odd) td {
background: #EBEBEB;
}
tr:nth-child(odd):hover td {
background: #4E5066;
}
td {
background: #FFFFFF;
padding: 20px;
text-align: left;
vertical-align: middle;
font-weight: 300;
font-size: 18px;
text-shadow: -1px -1px 1px rgba(0, 0, 0, 0.1);
border-right: 1px solid #C1C3D1;
}
td:last-child {
border-right: 0px;
}
tr:last-child td:first-child {
border-bottom-left-radius: 3px;
}
tr:last-child td:last-child {
border-bottom-right-radius: 3px;
}
/* Optional utility classes */
th.text-left, td.text-left {
text-align: left;
}
th.text-center, td.text-center {
text-align: center;
}
th.text-right, td.text-right {
text-align: right;
}
</style>
</head>
<body>
<div class="wrapper">
<header>CSV Table Viewer</header>
<input class="searchBox" placeholder="Search...">
<!-- CSV Template -->
<script id="csv-template" type="text/template">
{{CSV_DATA}}
</script>
<!-- Output Target -->
<div id="table-output"></div>
<footer>
<span id="footer-datetime">Add date-time here.</span>
</footer>
</div>
<script>
// Sample CSV input
let csvString = `
Name,Role,Department
Alice,Manager,HR
Bob,Engineer,Development
Carol,Analyst,Finance
`.trim();
let template = document.getElementById("csv-template").innerHTML;
let filledTemplate = template.replace('{{CSV_DATA}}', csvString);
function csvToJSON(csv) {
const lines = csv.split('\n').map(line => line.trim()).filter(line => line);
const headers = lines[0].split(',').map(h => h.trim());
const result = [];
for (let i = 1; i < lines.length; i++) {
const obj = {};
const currentline = lines[i].split(',').map(cell => cell.trim());
if (currentline.length !== headers.length) continue;
headers.forEach((key, j) => {
obj[key] = currentline[j];
});
result.push(obj);
}
return result;
}
function renderTableFromJSON(data, sortState = {}) {
if (!data || !data.length) return '';
const headers = Object.keys(data[0]);
let thead = '<thead><tr>' + headers.map(h => {
const sortClass = sortState.column === h
? (sortState.direction === 'asc' ? 'ascending' : 'descending')
: '';
return `<th class="${sortClass}" data-key="${h}"><span>${h}</span><div class="arrow_box"></div></th>`;
}).join('') + '</tr></thead>';
let tbody = '<tbody>' + data.map(row => {
return '<tr>' + headers.map(h => `<td data-th="${h}">${row[h]}</td>`).join('') + '</tr>';
}).join('') + '</tbody>';
return `<table class="table-fill">${thead}${tbody}</table>`;
}
let currentSort = { column: null, direction: 'asc' };
const searchInput = document.querySelector('.searchBox');
function attachSorting(dataSource) {
const headers = document.querySelectorAll('th[data-key]');
headers.forEach(th => {
th.addEventListener('click', () => {
const column = th.dataset.key;
if (currentSort.column === column) {
currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
} else {
currentSort.column = column;
currentSort.direction = 'asc';
}
const query = searchInput.value.trim().toLowerCase();
let workingData = dataSource;
if (query) {
workingData = dataSource.filter(row =>
Object.values(row).some(
value => value.toLowerCase().includes(query)
)
);
}
const sorted = [...workingData].sort((a, b) => {
const aVal = a[column] || '';
const bVal = b[column] || '';
return currentSort.direction === 'asc'
? aVal.localeCompare(bVal)
: bVal.localeCompare(aVal);
});
document.getElementById('table-output').innerHTML = renderTableFromJSON(sorted, currentSort);
attachSorting(dataSource); // rebind sorting events
});
});
}
const jsonData = csvToJSON(filledTemplate);
document.getElementById('table-output').innerHTML = renderTableFromJSON(jsonData);
attachSorting(jsonData);
searchInput.addEventListener('input', function () {
const query = this.value.trim().toLowerCase();
const filteredData = jsonData.filter(row =>
Object.values(row).some(
value => value.toLowerCase().includes(query)
)
);
document.getElementById('table-output').innerHTML = renderTableFromJSON(filteredData, currentSort);
attachSorting(jsonData);
});
console.log('Parsed JSON:', jsonData);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment