feat: create initial sport attendance system with Flask backend and interactive table UI

This commit is contained in:
Greg 2025-05-10 10:27:20 +02:00
parent a9fcda2bbb
commit e35c270f02
9 changed files with 255 additions and 1 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.env .env
node_modules node_modules
/Input
.windsurfrules .windsurfrules

View File

@ -0,0 +1,12 @@
# Coding pattern preferences
Always prefer simple solutions
Avoid duplication of code whenever possible, which means checking for other areas of the codebase that might already have similar code and functionality
You are careful to only make changes that are requested or you are confident are well understood and related to the change being requested
When fixing an issue or bug, do not introduce a new pattern or technology without first exhausting all options for the existing implementation. And if you finally do this, make sure to remove the old implementation afterwards so we don't have duplicate logic.
Keep the codebase very clean and organized
Avoid writing scripts in files if possible, especially if the script is likely only to be run once
Avoid having files over 200300 lines of code. Refactor at that point.
Mocking data is only needed for tests, never mock data for dev or prod
Never add stubbing or fake data patterns to code that affects the dev or prod environments
Never overwrite my .env file without first asking and confirming

View File

@ -0,0 +1,54 @@
**Core Features:**
1. **Attendance Table**
- A table with:
- Rows: Each game date (in DD/MM/YY format).
- Columns: Each players name (8 fixed names) and one column for a guest (labeled “Guest” or “Mystery”).
- Each cell (except for the date) is clickable to mark attendance (“Yes” or blank).
- The user should be able to adapt the name of the players.
2. **Add/Edit Dates**
- Ability to add a new date (row) to the table.
3. **Mark Attendance**
- Clicking a cell toggles attendance for that player/guest on that date.
- “Yes” means attending; blank means not attending.
- The user should be able to adapt the name of the guest.
- The user should be able to adapt the name of the players.
- The user should be able only to choose from Yes or blank to mark attendance.
4. **Data Persistence**
- The tables state (who is attending which date) is saved and loaded automatically (from a JSON file).
- No login or authentication needed (handled by your reverse proxy).
5. **Guest/Mystery Player**
- The last column is always for a guest. You can leave the name as “Guest” or allow it to be edited per date.
---
**What you dont need for MVP:**
- User registration, login, or roles.
- Notifications, reminders, or analytics.
- Player management (names are fixed).
- Complex UI-just a simple table.
---
**How it works (user flow):**
- User opens the web app.
- Sees a table with upcoming dates and player names.
- Clicks on their name under a date to mark themselves as attending (“Yes”).
- Guest attendance can also be marked.
- Data is saved automatically.
---
**Technical outline:**
- **Frontend:** Simple HTML table, checkboxes or clickable cells, minimal CSS.
- **Backend:** JSON file for storing attendance.
- **No authentication logic needed** (handled at the proxy level).
---
**Summary:**
You only need a simple, editable attendance table with dates as rows and player names (plus guest) as columns. Users can mark their attendance with a click. No login, no user management, just a fast and easy attendance tracker.

Binary file not shown.

46
app.py Normal file
View File

@ -0,0 +1,46 @@
from flask import Flask, render_template, request, jsonify
import json
import os
app = Flask(__name__)
DATA_FILE = 'attendance_data.json'
# Default player names
DEFAULT_PLAYERS = [
"Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Hannah"
]
DEFAULT_GUEST = "Guest"
def load_data():
if not os.path.exists(DATA_FILE):
data = {
"players": DEFAULT_PLAYERS,
"guest": DEFAULT_GUEST,
"dates": [],
"attendance": {}
}
with open(DATA_FILE, 'w') as f:
json.dump(data, f)
with open(DATA_FILE, 'r') as f:
return json.load(f)
def save_data(data):
with open(DATA_FILE, 'w') as f:
json.dump(data, f, indent=2)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/api/data', methods=['GET'])
def get_data():
return jsonify(load_data())
@app.route('/api/data', methods=['POST'])
def update_data():
data = request.json
save_data(data)
return jsonify({"status": "success"})
if __name__ == '__main__':
app.run(debug=True)

21
attendance_data.json Normal file
View File

@ -0,0 +1,21 @@
{
"attendance": {
"08/05/25|8": true,
"08/05/25|4": true
},
"dates": [
"08/05/25",
"5=5"
],
"guest": "Guest",
"players": [
"Alice",
"Bob",
"Charlie",
"David",
"Eve",
"Frank",
"Grace",
"Hannah"
]
}

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
flask

98
static/app.js Normal file
View File

@ -0,0 +1,98 @@
let data = {};
function fetchData() {
fetch('/api/data').then(r => r.json()).then(d => {
data = d;
renderTable();
});
}
function saveData() {
fetch('/api/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
}
function renderTable() {
const container = document.getElementById('attendance-table');
container.innerHTML = '';
const table = document.createElement('table');
// Header row
const thead = document.createElement('thead');
const headRow = document.createElement('tr');
headRow.appendChild(document.createElement('th')).innerText = 'Date';
data.players.forEach((name, i) => {
const th = document.createElement('th');
const input = document.createElement('input');
input.type = 'text';
input.value = name;
input.onchange = e => {
data.players[i] = e.target.value;
saveData();
renderTable();
};
th.appendChild(input);
headRow.appendChild(th);
});
// Guest column
const guestTh = document.createElement('th');
const guestInput = document.createElement('input');
guestInput.type = 'text';
guestInput.value = data.guest;
guestInput.onchange = e => {
data.guest = e.target.value;
saveData();
renderTable();
};
guestTh.appendChild(guestInput);
headRow.appendChild(guestTh);
thead.appendChild(headRow);
table.appendChild(thead);
// Body rows
const tbody = document.createElement('tbody');
data.dates.forEach((date, rowIdx) => {
const tr = document.createElement('tr');
// Date cell
const dateTd = document.createElement('td');
dateTd.innerText = date;
tr.appendChild(dateTd);
// Player attendance
[...data.players, data.guest].forEach((player, colIdx) => {
const td = document.createElement('td');
td.className = 'clickable';
const key = `${date}|${colIdx}`;
if (data.attendance[key]) {
td.innerText = 'Yes';
td.classList.add('yes');
} else {
td.innerText = '';
}
td.onclick = () => {
if (data.attendance[key]) {
delete data.attendance[key];
} else {
data.attendance[key] = true;
}
saveData();
renderTable();
};
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(tbody);
container.appendChild(table);
}
document.getElementById('add-date').onclick = function() {
const date = prompt('Enter date (DD/MM/YY):');
if (date && !data.dates.includes(date)) {
data.dates.push(date);
saveData();
renderTable();
}
};
window.onload = fetchData;

23
templates/index.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Sport Attendance Sheet</title>
<style>
body { font-family: Arial, sans-serif; margin: 2em; }
table { border-collapse: collapse; width: 100%; margin-bottom: 1em; }
th, td { border: 1px solid #ccc; padding: 0.5em; text-align: center; }
th { background: #f0f0f0; }
.clickable { cursor: pointer; background: #e7f7e7; }
.yes { background: #b6e7b6; font-weight: bold; }
input[type="text"] { width: 90%; }
#add-date { margin-top: 1em; }
</style>
</head>
<body>
<h1>Sport Attendance Sheet</h1>
<div id="attendance-table"></div>
<button id="add-date">Add Date</button>
<script src="/static/app.js"></script>
</body>
</html>