Add demo-04: signature list generator with code cleanup task

This commit is contained in:
Benjamin Hackl 2026-01-15 23:02:09 +01:00
commit caed60a2d9
5 changed files with 405 additions and 0 deletions

49
demo-04/README.md Normal file
View file

@ -0,0 +1,49 @@
# Demo 04: Signature List Generator - Code Cleanup
## Purpose
This is a static web application that turns student lists exported from UNIGRAZonline / CAMPUSonline (as "CSV for Excel") into nicely formatted signature lists for checking attendance. Everything happens locally in the browser - no sensitive data is transmitted anywhere.
**Features:**
- Upload CSV student lists exported from UNIGRAZonline
- Filter by group name (manually entered or extracted from file)
- Customizable course name and date
- Include/exclude Uni Graz logo
- Print-ready signature lists
- Privacy-first: all processing happens client-side
## Technology Stack
- **Frontend:** Pure HTML5, CSS3, and vanilla JavaScript
- **Styling:** Bootstrap 5.0.2 (via CDN)
- **CSV Parsing:** PapaParse 5.4.1 (via CDN)
- **Fonts:** Nunito Sans (Uni Graz corporate font)
## Task: Code Cleanup
This codebase needs cleanup and modernization. Your goal is to iteratively improve the codebase by:
1. **Fixing bugs and issues** (syntax errors, missing semicolons, etc.)
2. **Modernizing JavaScript** (replace `var` with `const`/`let`, use modern patterns)
3. **Adding error handling** (invalid CSV, missing columns, edge cases)
4. **Improving accessibility** (ARIA labels, keyboard navigation)
5. **Code quality improvements** (consistent styling, remove duplication, better structure)
## Process
Work with the agent to identify issues and iteratively improve the code. For each change:
- Explain what you're fixing and why
- Get the agent's approval before making changes
- Test the changes to ensure nothing breaks
- Document your changes in a CHANGES.md file
## Files
- `index.html` - Main HTML structure
- `main.css` - Styling and print styles
- `main.js` - Application logic
- `logo.png` - Uni Graz logo
## Deployment
This is a static site - simply upload all files to a web server. No build process or backend required.

120
demo-04/index.html Normal file
View file

@ -0,0 +1,120 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link href="main.css" rel="stylesheet">
<meta name="description" content="Generator turning UNIGRAZonline / CAMPUSonline based student lists into nicely formatted signature lists for checking attendance">
<meta name="author" content="Benjamin Hackl">
<title>Signature List Generator</title>
</head>
<body>
<nav class="navbar navbar-light bg-unigraz">
<div class="container-fluid">
<a class="navbar-brand" href="https://imsc.uni-graz.at">Math @ Uni Graz</a>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="#contactModal" data-bs-toggle="modal">Feedback / Contact</a>
</li>
</ul>
</div>
</nav>
<section class="generator-input container mt-3">
<h1>Signature List Generator</h1>
<p>
<strong>Important:</strong>
Everything happens in your browser and locally on your computer,
no sensitive data is transmitted anywhere.
</p>
<div class="form-group mb-3">
<div class="row mb-3">
<div class="col-12">
<label for="lvname" class="form-label">Course name</label>
<input id="lvname" type="text" class="form-control" value="MAT.001: Meine tolle Lehrveranstaltung">
</div>
</div>
<div class="row mb-3" style="align-items: baseline;">
<div class="col-12 col-md-8">
<label for="groupname" class="form-label">Group name</label>
<input id="groupname" type="text" class="form-control" placeholder="Group name" value="Gruppe 42">
<select class="form-select" hidden disabled id="groupname-select">
</select>
</div>
<div class="col-12 col-md-4">
<label for="date" class="form-label">Date on list</label>
<input id="date" type="date" class="form-control">
</div>
</div>
<div class="row mb-3">
<div class="col-auto">
<input type="checkbox" class="form-check-input" id="group_from_file">
<label class="form-check-label" for="group_from_file">
Filter by group name from file
</label>
</div>
<div class="col-auto">
<input checked type="checkbox" class="form-check-input" id="include_logo">
<label class="form-check-label" for="include_logo">
Include logo on list
</label>
</div>
</div>
<div class="row">
<div class="col-12 col-md-9 mb-3">
<label for="fileinput" class="form-label">List of students exported from UNIGRAZonline (as <i>CSV for Excel</i>)</label>
<input id="fileinput" type="file" class="form-control">
</div>
<div class="col-12 col-md-3 mb-3 d-flex align-items-end">
<button id="printbtn" class="btn btn-unigraz" onclick="window.print();">
Print table</button>
</div>
</div>
<hr>
</div>
</section>
<section class="container">
<img id="logo" src="logo.png" height="60">
<h5 id="list-caption"></h5>
<h6 id="list-subcaption"></h6>
<table>
<thead>
<tr>
<th>Name</th>
<th>Unterschrift</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
<tfoot>
<tr><td colspan="2">
<span id="foot-title"></span>
<span id="foot-group-date"></span>
</td></tr>
</tfoot>
</table>
</section>
<div class="modal fade" id="contactModal" tabindex="-1" aria-labelledby="contactModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="contactModalLabel">Contact / Feedback</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Please send bug reports, feature requests, and other related feedback directly to <a href="mailto:benjamin.hackl@uni-graz.at">Benjamin Hackl</a>&mdash;or message me via <a href="https://matrix.to/#/@benjamin.hackl:uni-graz.at">uniCHAT / matrix</a>.
</div>
<div class="modal-footer">
Made with
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#af4e37" class="bi bi-heart-fill" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z"/>
</svg>
by <a href="https://behackl.dev">behackl.dev</a>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js" integrity="sha512-dfX5uYVXzyU8+KHqj8bjo7UkOdg18PaOtpa48djpNbZHwExddghZ+ZmzWT06R5v6NSk3ZUfsH6FNEDepLx9hPQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="main.js"></script>
</body>
</html>

BIN
demo-04/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

116
demo-04/main.css Normal file
View file

@ -0,0 +1,116 @@
:root {
--unigraz-primary: #ffd400;
}
@font-face {
font-family: nunito_sans;
font-display: swap;
font-style: normal;
font-display: block;
font-weight: 300;
src: url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300.eot);
src: url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300.eot?#iefix) format("embedded-opentype"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300.woff2) format("woff2"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300.woff) format("woff"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300.ttf) format("truetype"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300.svg#NunitoSans) format("svg")
}
@font-face {
font-display: swap;
font-family: nunito_sans;
font-style: italic;
font-display: block;
font-weight: 300;
src: url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300italic.eot);
src: url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300italic.eot?#iefix) format("embedded-opentype"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300italic.woff2) format("woff2"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300italic.woff) format("woff"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300italic.ttf) format("truetype"),url(https://static.uni-graz.at/dist/unigraz/fonts/Nunito_Sans/nunito-sans-v12-latin_latin-ext-300italic.svg#NunitoSans) format("svg")
}
body {
font-family: nunito_sans;
}
.bg-unigraz {
background-color: var(--unigraz-primary);
}
#printbtn {
flex: 1;
}
.btn-unigraz {
color: #000;
background-color: var(--unigraz-primary);
border-color: var(--unigraz-primary);
}
.btn-unigraz:active, .btn-unigraz:hover {
color: #000;
background-color: #ffe24f;
border-color: #ffe24f;
}
.navbar-brand {
font-weight: 600;
}
.navbar-light .navbar-nav .nav-link {
color: rgba(0, 0, 0, .85);
}
#logo {
float: right;
}
table {
width: 100%;
}
th {
border-right: 1px solid black;
border-bottom: 2px solid black;
padding: 5px;
}
td {
height: 0.8cm;
font-size: small;
border-right: 1px solid black;
border-bottom: 1px solid black;
padding: 5px;
}
tr > th:last-child, td:last-child {
border-right: none;
}
tr > td:first-child {
width: 20%;
white-space: nowrap;
padding-right: 15px;
}
tfoot td {
font-size: smaller;
border-bottom: none;
}
#list-footer {
display: flex;
}
#foot-group-date {
float: right;
}
@media print {
table {
margin-bottom: 10cm;
}
.navbar, .generator-input, #contactModal {
display: none !important;
}
.container {
max-width: 90%;
}
#list-footer {
position: fixed;
bottom: -0.25cm;
}
}

120
demo-04/main.js Normal file
View file

@ -0,0 +1,120 @@
var DATA_STORE = {};
function populateTable() {
let file_list = document.getElementById('fileinput').files;
if (file_list.length > 0) {
let csv_file = file_list[0];
Papa.parse(csv_file, {
header: true,
complete: function(results) {
DATA_STORE['participants'] = results.data.filter(teilnehmer => teilnehmer["Familienname"] !== undefined);
DATA_STORE['groupnames'] = new Set(DATA_STORE['participants'].map(teilnehmer => teilnehmer["Gruppe"]));
const group_select = document.getElementById('groupname-select');
group_select.innerHTML = '';
var default_option = document.createElement('option');
default_option.text = "Choose Group...";
default_option.selected = true;
group_select.appendChild(default_option);
DATA_STORE['groupnames'].forEach(group => {
var option = document.createElement('option');
option.value = group;
option.text = group;
group_select.appendChild(option);
});
updateCaptions();
updateTable();
}
});
}
}
function updateTable(filter_group) {
const table_body = document.getElementById('table-body');
table_body.innerHTML = '';
DATA_STORE["participants"].forEach(teilnehmer => {
if (filter_group === undefined || (filter_group !== undefined && teilnehmer["Gruppe"] == filter_group)) {
var table_row = document.createElement('tr');
var name = document.createElement('td');
name.innerHTML = teilnehmer["Familienname"] + ", " + teilnehmer["Vorname"];
table_row.appendChild(name);
table_row.appendChild(document.createElement('td'));
table_body.appendChild(table_row);
}
},
false
);
}
function updateCaptions() {
let lvname = document.getElementById('lvname').value;
document.getElementById('list-caption').innerHTML = lvname;
document.getElementById('foot-title').innerHTML = lvname;
var groupname, subcaption;
if (document.getElementById('group_from_file').checked) {
groupname = document.getElementById('groupname-select').value;
} else {
groupname = document.getElementById('groupname').value;
}
let listdate = document.getElementById('date').value;
if (listdate != "") {
listdate = new Date(listdate);
subcaption = `${groupname}, ${listdate.toLocaleDateString('de-AT')}`;
} else {
subcaption = groupname;
}
document.getElementById('list-subcaption').innerHTML = subcaption;
document.getElementById('foot-group-date').innerHTML = subcaption;
}
document.getElementById('fileinput').addEventListener('change', populateTable);
document.querySelectorAll('input, select').forEach(
elem => elem.addEventListener('input', updateCaptions),
false
);
document.querySelectorAll('select').forEach(
elem => elem.addEventListener('input', () => {
updateCaptions();
updateTable(elem.value);
}),
false
);
document.getElementById('group_from_file').addEventListener('change', (evt) => {
const groupname_input = document.getElementById('groupname');
const groupname_select = document.getElementById('groupname-select');
if (evt.target.checked) {
groupname_input.disabled = true;
groupname_input.hidden = true;
groupname_select.disabled = false;
groupname_select.hidden = false;
} else {
groupname_input.disabled = false;
groupname_input.hidden = false;
groupname_select.hidden = true;
groupname_select.disabled = true;
updateTable();
}
});
document.getElementById('include_logo').addEventListener('change', (evt) => {
if (evt.target.checked) {
document.getElementById('logo').style['display'] = 'block';
} else {
document.getElementById('logo').style['display'] = 'none';
}
});
document.addEventListener("DOMContentLoaded", function() {
updateCaptions();
});