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
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fastapi
uvicorn
httpx
watchfiles
watchfiles
pytest
56 changes: 55 additions & 1 deletion src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,42 @@
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
},
"Basketball Team": {
"description": "Competitive basketball team and practice sessions",
"schedule": "Tuesdays and Thursdays, 4:30 PM - 6:00 PM",
"max_participants": 15,
"participants": ["james@mergington.edu"]
},
"Soccer Club": {
"description": "Soccer training and friendly matches",
"schedule": "Mondays and Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 25,
"participants": ["lucas@mergington.edu", "ava@mergington.edu"]
},
"Art Studio": {
"description": "Painting, drawing, and visual arts creation",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 18,
"participants": ["isabella@mergington.edu"]
},
"Music Band": {
"description": "Learn to play instruments and perform in concerts",
"schedule": "Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 20,
"participants": ["noah@mergington.edu", "mia@mergington.edu"]
},
"Debate Team": {
"description": "Competitive debate and public speaking skills",
"schedule": "Mondays and Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["alexander@mergington.edu"]
},
"Science Club": {
"description": "Explore STEM topics and conduct experiments",
"schedule": "Fridays, 2:00 PM - 3:30 PM",
"max_participants": 22,
"participants": ["charlotte@mergington.edu", "benjamin@mergington.edu"]
}
}

Expand All @@ -54,14 +90,32 @@ def get_activities():

@app.post("/activities/{activity_name}/signup")
def signup_for_activity(activity_name: str, email: str):
"""Sign up a student for an activity"""
# Validate activity exists
if activity_name not in activities:
raise HTTPException(status_code=404, detail="Activity not found")

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

# Get the specific activity
activity = activities[activity_name]

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


@app.delete("/activities/{activity_name}/unregister")
def unregister_from_activity(activity_name: str, email: str):
# Validate activity exists
if activity_name not in activities:
raise HTTPException(status_code=404, detail="Activity not found")

# Validate student is signed up
if email not in activities[activity_name]["participants"]:
raise HTTPException(status_code=404, detail="Participant not found in activity")

# Remove student
activities[activity_name]["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
84 changes: 74 additions & 10 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,84 @@ document.addEventListener("DOMContentLoaded", () => {

// Clear loading message
activitiesList.innerHTML = "";
// Reset activity select options
activitySelect.innerHTML = '<option value="">-- Select an activity --</option>';

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

const spotsLeft = details.max_participants - details.participants.length;
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>
`;
const participants = details.participants || [];
const participantsHTML = participants.length
? `
<div class="participants">
<h5>Participants</h5>
<ul class="participants-list">
${participants
.map(
(p) =>
`<li><span class="participant-email">${p}</span><button class="unregister-btn" data-activity="${name}" data-email="${p}" aria-label="Unregister ${p}">🗑️</button></li>`
)
.join("")}
</ul>
</div>
`
: `
<div class="participants">
<h5>Participants</h5>
<p class="no-participants">No participants yet</p>
</div>
`;

activitiesList.appendChild(activityCard);
activityCard.innerHTML = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
${participantsHTML}
`;

activitiesList.appendChild(activityCard);

// Attach unregister handlers for this activity card
activityCard.querySelectorAll(".unregister-btn").forEach((btn) => {
btn.addEventListener("click", async (e) => {
const email = btn.dataset.email;
const activity = btn.dataset.activity;

try {
const resp = await fetch(
`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(
email
)}`,
{ method: "DELETE" }
);

const result = await resp.json();

if (resp.ok) {
messageDiv.textContent = result.message;
messageDiv.className = "success";
messageDiv.classList.remove("hidden");
setTimeout(() => messageDiv.classList.add("hidden"), 3000);
// Refresh activities list
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
}
} catch (err) {
console.error("Error unregistering:", err);
messageDiv.textContent = "Failed to unregister. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
}
});
});

// Add option to select dropdown
const option = document.createElement("option");
Expand Down Expand Up @@ -62,6 +124,8 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
// Refresh activities so the new participant appears without page reload
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
Expand Down
43 changes: 43 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,49 @@ section h3 {
margin-bottom: 8px;
}

.participants {
margin-top: 12px;
}

.participants h5 {
margin-bottom: 8px;
color: #333;
font-size: 15px;
}

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

.participants-list li {
padding: 4px 0;
color: #555;
font-size: 14px;
display: flex;
align-items: center;
justify-content: space-between;
}

.unregister-btn {
background: transparent;
border: none;
color: #c62828;
cursor: pointer;
font-size: 14px;
padding: 4px;
}

.unregister-btn:hover {
opacity: 0.8;
}

.no-participants {
font-style: italic;
color: #777;
}

.form-group {
margin-bottom: 15px;
}
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# tests package
Loading
Loading