Last active
March 24, 2025 15:56
-
-
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.
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"> | |
<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