feat: create initial sport attendance system with Flask backend and interactive table UI
This commit is contained in:
parent
a9fcda2bbb
commit
e35c270f02
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
.env
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
/Input
|
|
||||||
.windsurfrules
|
.windsurfrules
|
||||||
12
Input/Coding patterns preferences
Normal file
12
Input/Coding patterns preferences
Normal 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 200–300 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
|
||||||
54
Input/Sport Attendance Sheet MVP.md
Normal file
54
Input/Sport Attendance Sheet MVP.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
**Core Features:**
|
||||||
|
|
||||||
|
1. **Attendance Table**
|
||||||
|
- A table with:
|
||||||
|
- Rows: Each game date (in DD/MM/YY format).
|
||||||
|
- Columns: Each player’s 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 table’s 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 don’t 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.
|
||||||
BIN
Input/Sport Attendance Sheet.xlsx
Normal file
BIN
Input/Sport Attendance Sheet.xlsx
Normal file
Binary file not shown.
46
app.py
Normal file
46
app.py
Normal 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
21
attendance_data.json
Normal 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
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
flask
|
||||||
98
static/app.js
Normal file
98
static/app.js
Normal 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
23
templates/index.html
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user