Add demo-04: signature list generator with code cleanup task
This commit is contained in:
parent
fe1638cacb
commit
caed60a2d9
5 changed files with 405 additions and 0 deletions
49
demo-04/README.md
Normal file
49
demo-04/README.md
Normal 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
120
demo-04/index.html
Normal 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>—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
BIN
demo-04/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
116
demo-04/main.css
Normal file
116
demo-04/main.css
Normal 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
120
demo-04/main.js
Normal 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();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue