From 34393be872920e355985f1d391e81c87fd6db2ff Mon Sep 17 00:00:00 2001
From: ValiDM <44869004+ValiDM@users.noreply.github.com>
Date: Fri, 19 Dec 2025 16:42:21 +0000
Subject: [PATCH 1/3] Add styles for the participants section and list to make
it visually appealing and consistent.
---
src/static/app.js | 20 ++++++++++++++++++++
src/static/styles.css | 27 +++++++++++++++++++++++++++
2 files changed, 47 insertions(+)
diff --git a/src/static/app.js b/src/static/app.js
index dcc1e38..0e00942 100644
--- a/src/static/app.js
+++ b/src/static/app.js
@@ -20,11 +20,31 @@ document.addEventListener("DOMContentLoaded", () => {
const spotsLeft = details.max_participants - details.participants.length;
+ // Participants section
+ let participantsSection = "";
+ if (details.participants.length > 0) {
+ participantsSection = `
+
+
Participants:
+
+ ${details.participants.map(email => `- ${email}
`).join("")}
+
+
+ `;
+ } else {
+ participantsSection = `
+
+ No participants yet.
+
+ `;
+ }
+
activityCard.innerHTML = `
${name}
${details.description}
Schedule: ${details.schedule}
Availability: ${spotsLeft} spots left
+ ${participantsSection}
`;
activitiesList.appendChild(activityCard);
diff --git a/src/static/styles.css b/src/static/styles.css
index a533b32..515fad3 100644
--- a/src/static/styles.css
+++ b/src/static/styles.css
@@ -74,6 +74,33 @@ section h3 {
margin-bottom: 8px;
}
+/* Add some spacing for the participants section */
+.participants-section {
+ margin-top: 12px;
+ padding: 10px;
+ background-color: #eef2fa;
+ border-radius: 4px;
+}
+
+.participants-section strong {
+ color: #3949ab;
+ display: block;
+ margin-bottom: 6px;
+}
+
+.participants-list {
+ margin-left: 18px;
+ margin-bottom: 0;
+ color: #333;
+}
+
+.participants-section.no-participants {
+ background-color: #f5f5f5;
+ color: #888;
+ font-style: italic;
+ padding: 8px 10px;
+}
+
.form-group {
margin-bottom: 15px;
}
From 04d75ac3d9dea2400816ad20fcc0820c3232b92d Mon Sep 17 00:00:00 2001
From: ValiDM <44869004+ValiDM@users.noreply.github.com>
Date: Fri, 19 Dec 2025 17:02:02 +0000
Subject: [PATCH 2/3] feat: add participant delete icon, hide bullets, and
auto-refresh activities
Added a delete icon next to each participant for unregistering from activities
Hid bullet points in the participant list for cleaner UI
Implemented auto-refresh of activities after participant registration or deletion
Added missing backend endpoints to support these features
---
src/app.py | 22 ++++++++++++++++++++++
src/static/app.js | 32 +++++++++++++++++++++++++++++++-
src/static/styles.css | 28 +++++++++++++++++++++++++++-
3 files changed, 80 insertions(+), 2 deletions(-)
diff --git a/src/app.py b/src/app.py
index cab7d58..ebebb88 100644
--- a/src/app.py
+++ b/src/app.py
@@ -10,10 +10,21 @@
from fastapi.responses import RedirectResponse
import os
from pathlib import Path
+from fastapi.responses import JSONResponse
+
app = FastAPI(title="Mergington High School API",
description="API for viewing and signing up for extracurricular activities")
+# Redirect root URL to static index.html
+@app.get("/")
+def root():
+ return RedirectResponse(url="/static/index.html")
+
+@app.get("/activities")
+def get_activities():
+ return JSONResponse(content=activities)
+
# Mount the static files directory
current_dir = Path(__file__).parent
app.mount("/static", StaticFiles(directory=os.path.join(Path(__file__).parent,
@@ -83,6 +94,17 @@
}
}
+@app.delete("/activities/{activity_name}/unregister")
+def unregister_from_activity(activity_name: str, email: str):
+ """Remove a student from an activity"""
+ if activity_name not in activities:
+ raise HTTPException(status_code=404, detail="Activity not found")
+ activity = activities[activity_name]
+ if email not in activity["participants"]:
+ raise HTTPException(status_code=404, detail="Participant not found in this activity")
+ activity["participants"].remove(email)
+ return {"message": f"Removed {email} from {activity_name}"}
+
@app.post("/activities/{activity_name}/signup")
def signup_for_activity(activity_name: str, email: str):
"""Sign up a student for an activity"""
diff --git a/src/static/app.js b/src/static/app.js
index 0e00942..c71a87c 100644
--- a/src/static/app.js
+++ b/src/static/app.js
@@ -27,7 +27,12 @@ document.addEventListener("DOMContentLoaded", () => {
Participants:
- ${details.participants.map(email => `- ${email}
`).join("")}
+ ${details.participants.map(email => `
+ -
+ ${email}
+
+
+ `).join("")}
`;
@@ -49,6 +54,30 @@ document.addEventListener("DOMContentLoaded", () => {
activitiesList.appendChild(activityCard);
+ // Add event listeners for delete buttons (after DOM insertion)
+ setTimeout(() => {
+ activityCard.querySelectorAll('.delete-participant-btn').forEach(btn => {
+ btn.addEventListener('click', async (e) => {
+ const activity = btn.getAttribute('data-activity');
+ const email = btn.getAttribute('data-email');
+ if (!confirm(`Remove ${email} from ${activity}?`)) return;
+ try {
+ const response = await fetch(`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`, {
+ method: 'DELETE',
+ });
+ const result = await response.json();
+ if (response.ok) {
+ fetchActivities();
+ } else {
+ alert(result.detail || 'Failed to remove participant.');
+ }
+ } catch (err) {
+ alert('Error removing participant.');
+ }
+ });
+ });
+ }, 0);
+
// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
@@ -82,6 +111,7 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
+ fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
diff --git a/src/static/styles.css b/src/static/styles.css
index 515fad3..edc0c04 100644
--- a/src/static/styles.css
+++ b/src/static/styles.css
@@ -89,9 +89,35 @@ section h3 {
}
.participants-list {
- margin-left: 18px;
+ margin-left: 0;
margin-bottom: 0;
color: #333;
+ list-style: none;
+ padding-left: 0;
+ /* Remove default bullets */
+}
+
+.participant-item {
+ list-style: none;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.delete-participant-btn {
+ background: none;
+ border: none;
+ color: #c62828;
+ cursor: pointer;
+ font-size: 18px;
+ line-height: 1;
+ padding: 0 4px;
+ margin-left: 4px;
+ transition: color 0.2s;
+}
+.delete-participant-btn:hover {
+ color: #ff1744;
+}
}
.participants-section.no-participants {
From 4bc1e8279735fd9e5cfd7baf237b3b4ccba440dd Mon Sep 17 00:00:00 2001
From: ValiDM <44869004+ValiDM@users.noreply.github.com>
Date: Fri, 19 Dec 2025 17:08:03 +0000
Subject: [PATCH 3/3] test: add FastAPI API tests with pytest
Added test_app.py with basic FastAPI endpoint tests using pytest and httpx
Updated requirements.txt to include pytest and httpx for testing
Ensured tests directory structure for future test expansion
---
requirements.txt | 4 ++++
src/static/styles.css | 1 -
tests/test_app.py | 40 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 44 insertions(+), 1 deletion(-)
create mode 100644 tests/test_app.py
diff --git a/requirements.txt b/requirements.txt
index 97dc7cd..9078b9f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,6 @@
+# For testing
+pytest
+httpx
+pytest-asyncio
fastapi
uvicorn
diff --git a/src/static/styles.css b/src/static/styles.css
index edc0c04..49c2509 100644
--- a/src/static/styles.css
+++ b/src/static/styles.css
@@ -118,7 +118,6 @@ section h3 {
.delete-participant-btn:hover {
color: #ff1744;
}
-}
.participants-section.no-participants {
background-color: #f5f5f5;
diff --git a/tests/test_app.py b/tests/test_app.py
new file mode 100644
index 0000000..81c8b46
--- /dev/null
+++ b/tests/test_app.py
@@ -0,0 +1,40 @@
+import pytest
+from fastapi.testclient import TestClient
+from src.app import app
+
+client = TestClient(app)
+
+def test_get_activities():
+ response = client.get("/activities")
+ assert response.status_code == 200
+ data = response.json()
+ assert isinstance(data, dict)
+ assert "Chess Club" in data
+
+def test_signup_and_unregister():
+ test_email = "testuser@mergington.edu"
+ activity = "Chess Club"
+ # Ensure user is not already signed up
+ client.delete(f"/activities/{activity}/unregister", params={"email": test_email})
+ # Sign up
+ response = client.post(f"/activities/{activity}/signup", params={"email": test_email})
+ assert response.status_code == 200
+ assert f"Signed up {test_email}" in response.json()["message"]
+ # Unregister
+ response = client.delete(f"/activities/{activity}/unregister", params={"email": test_email})
+ assert response.status_code == 200
+ assert f"Removed {test_email}" in response.json()["message"]
+
+def test_signup_duplicate():
+ test_email = "duplicate@mergington.edu"
+ activity = "Chess Club"
+ # Clean up
+ client.delete(f"/activities/{activity}/unregister", params={"email": test_email})
+ # First signup
+ client.post(f"/activities/{activity}/signup", params={"email": test_email})
+ # Duplicate signup
+ response = client.post(f"/activities/{activity}/signup", params={"email": test_email})
+ assert response.status_code == 400
+ assert "already signed up" in response.json()["detail"]
+ # Clean up
+ client.delete(f"/activities/{activity}/unregister", params={"email": test_email})