Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
fastapi
uvicorn

# Test dependencies
pytest
requests
60 changes: 60 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
},
# Sports-related activities
"Soccer Team": {
"description": "Competitive soccer team practicing tactics and fitness",
"schedule": "Mondays, Wednesdays, 4:00 PM - 6:00 PM",
"max_participants": 22,
"participants": ["alex@mergington.edu", "nina@mergington.edu"]
},
"Basketball Team": {
"description": "Team practices and inter-school games",
"schedule": "Tuesdays, Thursdays, 4:00 PM - 6:00 PM",
"max_participants": 15,
"participants": ["tyler@mergington.edu", "maya@mergington.edu"]
},
# Artistic activities
"Drama Club": {
"description": "Acting, script work, and stage production for school plays",
"schedule": "Wednesdays, 3:30 PM - 5:30 PM",
"max_participants": 25,
"participants": ["lucy@mergington.edu", "robert@mergington.edu"]
},
"Music Ensemble": {
"description": "Instrumental and vocal ensemble rehearsals and performances",
"schedule": "Fridays, 4:00 PM - 6:00 PM",
"max_participants": 30,
"participants": ["isabella@mergington.edu", "henry@mergington.edu"]
},
# Intellectual activities
"Debate Team": {
"description": "Practice formal debating, public speaking, and research",
"schedule": "Tuesdays, 5:00 PM - 6:30 PM",
"max_participants": 18,
"participants": ["oliver@mergington.edu", "grace@mergington.edu"]
},
"Science Club": {
"description": "Hands-on experiments, science fairs, and research projects",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["liam@mergington.edu", "ava@mergington.edu"]
}
}

Expand All @@ -62,6 +101,27 @@ def signup_for_activity(activity_name: str, email: str):
# Get the specific activity
activity = activities[activity_name]

# Validate student is not already signed up
if email in activity["participants"]:
raise HTTPException(status_code=400, detail="Student already signed up for this activity")

# Add student
activity["participants"].append(email)
return {"message": f"Signed up {email} for {activity_name}"}


@app.delete("/activities/{activity_name}/participants")
def unregister_participant(activity_name: str, email: str):
"""Unregister a student from an activity"""
# Validate activity exists
if activity_name not in activities:
raise HTTPException(status_code=404, detail="Activity not found")

activity = activities[activity_name]

# Check participant exists
if email not in activity["participants"]:
raise HTTPException(status_code=404, detail="Participant not found in activity")

activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
96 changes: 89 additions & 7 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,101 @@ document.addEventListener("DOMContentLoaded", () => {
// Clear loading message
activitiesList.innerHTML = "";

// Clear existing options except placeholder
// Keep the first placeholder option if it exists
while (activitySelect.options.length > 1) {
activitySelect.remove(1);
}

// Populate activities list
Object.entries(activities).forEach(([name, details]) => {
const activityCard = document.createElement("div");
activityCard.className = "activity-card";

const spotsLeft = details.max_participants - details.participants.length;

activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
`;
// Build activity card skeleton
activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p class="availability"><strong>Availability:</strong> ${spotsLeft} spots left</p>
<div class="participants-section">
<strong>Participants:</strong>
<div class="participants-container"></div>
</div>
`;

activitiesList.appendChild(activityCard);

// Populate participants list programmatically so we can attach handlers
const participantsContainer = activityCard.querySelector(
".participants-container"
);

if (details.participants && details.participants.length > 0) {
const ul = document.createElement("ul");
ul.className = "participants-list";

details.participants.forEach((p) => {
const li = document.createElement("li");
li.className = "participant-item";
const span = document.createElement("span");
span.textContent = p;

const btn = document.createElement("button");
btn.className = "delete-btn";
btn.setAttribute("aria-label", `Unregister ${p}`);
btn.title = "Unregister participant";
btn.innerHTML = "&#x2716;"; // heavy multiplication X

// Click handler to unregister
btn.addEventListener("click", async () => {
try {
const resp = await fetch(
`/activities/${encodeURIComponent(name)}/participants?email=${encodeURIComponent(p)}`,
{ method: "DELETE" }
);

if (resp.ok) {
// remove li from DOM
li.remove();

// If no more participants, show empty hint
if (ul.querySelectorAll("li").length === 0) {
participantsContainer.innerHTML =
'<div class="participants-empty">No participants yet.</div>';
}

// Update availability counter
const availEl = activityCard.querySelector(".availability");
if (availEl) {
const match = availEl.textContent.match(/(\d+) spots left/);
if (match) {
const current = parseInt(match[1], 10);
availEl.textContent = `Availability: ${current + 1} spots left`;
}
}
} else {
const result = await resp.json();
alert(result.detail || "Failed to unregister participant");
}
} catch (err) {
console.error("Error unregistering participant:", err);
alert("Network error while unregistering participant");
}
});

li.appendChild(span);
li.appendChild(btn);
ul.appendChild(li);
});

activitiesList.appendChild(activityCard);
participantsContainer.appendChild(ul);
} else {
participantsContainer.innerHTML =
'<div class="participants-empty">No participants yet.</div>';
}

// Add option to select dropdown
const option = document.createElement("option");
Expand Down Expand Up @@ -62,6 +142,8 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
// Refresh activities so the newly signed up participant appears immediately
await fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand Down
60 changes: 60 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ header {
border-radius: 5px;
}


header h1 {
margin-bottom: 10px;
}
Expand Down Expand Up @@ -142,3 +143,62 @@ footer {
padding: 20px;
color: #666;
}

.participants-section {
margin-top: 12px;
padding-top: 12px;
border-top: 1px dashed #eee;
}

.participants-section strong {
display: block;
margin-bottom: 8px;
color: #333;
}

.participants-list {
list-style: none;
padding-left: 0;
margin: 0;
display: block;
}

.participants-list li,
.participant-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
padding: 6px 8px;
background: linear-gradient(180deg, #ffffff, #fbfbfb);
border-radius: 4px;
border: 1px solid #f0f0f0;
color: #444;
font-size: 14px;
}

.participants-list .delete-btn,
.participant-item .delete-btn {
background: transparent;
border: none;
color: #c62828;
cursor: pointer;
font-size: 16px;
line-height: 1;
padding: 4px 6px;
border-radius: 4px;
}

.participants-list .delete-btn:hover,
.participant-item .delete-btn:hover {
color: #e53935;
background: rgba(229, 57, 53, 0.06);
}

/* A subtle "no participants" hint */
.participants-empty {
font-style: italic;
color: #777;
padding: 6px 8px;
}
53 changes: 53 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pytest
from fastapi.testclient import TestClient

from src.app import app, activities


client = TestClient(app)


def test_get_activities():
resp = client.get("/activities")
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, dict)
assert "Chess Club" in data


def test_signup_and_get_updates():
activity = "Chess Club"
email = "test_student@example.com"

# Ensure clean state
if email in activities[activity]["participants"]:
activities[activity]["participants"].remove(email)

resp = client.post(f"/activities/{activity}/signup?email={email}")
assert resp.status_code == 200
result = resp.json()
assert "Signed up" in result["message"]

# Verify the in-memory activities reflect the new participant
resp2 = client.get("/activities")
data = resp2.json()
assert email in data[activity]["participants"]


def test_unregister_participant():
activity = "Chess Club"
email = "test_student_to_remove@example.com"

# Ensure participant exists first
if email not in activities[activity]["participants"]:
activities[activity]["participants"].append(email)

resp = client.delete(f"/activities/{activity}/participants?email={email}")
assert resp.status_code == 200
result = resp.json()
assert "Unregistered" in result["message"]

# Confirm removal
resp2 = client.get("/activities")
data = resp2.json()
assert email not in data[activity]["participants"]