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
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
fastapi
uvicorn
httpx
pytest
pytest-asyncio
59 changes: 59 additions & 0 deletions 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"]
},
"Soccer Team": {
"description": "Competitive soccer training and matches against other schools",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 22,
"participants": ["lucas@mergington.edu", "liam@mergington.edu"]
},
"Swimming Club": {
"description": "Swim training for all skill levels and competitive meets",
"schedule": "Mondays and Wednesdays, 6:00 AM - 7:30 AM",
"max_participants": 20,
"participants": ["mia@mergington.edu", "noah@mergington.edu"]
},
"Art Studio": {
"description": "Explore painting, drawing, and mixed media techniques",
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 15,
"participants": ["Isabella@mergington.edu", "ava@mergington.edu"]
},
"Drama Club": {
"description": "Act, direct, and produce theatrical performances",
"schedule": "Mondays and Fridays, 4:00 PM - 5:30 PM",
"max_participants": 25,
"participants": ["charlotte@mergington.edu", "amelia@mergington.edu"]
},
"Math Olympiad": {
"description": "Prepare for math competitions and solve advanced problems",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 15,
"participants": ["ethan@mergington.edu", "harper@mergington.edu"]
},
"Science Club": {
"description": "Conduct experiments and explore STEM topics beyond the classroom",
"schedule": "Fridays, 3:30 PM - 5:00 PM",
"max_participants": 20,
"participants": ["james@mergington.edu", "sofia@mergington.edu"]
}
}

Expand All @@ -62,6 +98,29 @@ 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}/signup")
def unregister_from_activity(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")

# Get the specific activity
activity = activities[activity_name]

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

# Remove student
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
55 changes: 54 additions & 1 deletion src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,42 @@ document.addEventListener("DOMContentLoaded", () => {
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
<p><strong>Availability:</strong> <span class="spots-badge ${spotsLeft === 0 ? 'spots-full' : spotsLeft <= 3 ? 'spots-low' : 'spots-ok'}">${spotsLeft} spot${spotsLeft !== 1 ? 's' : ''} left</span></p>
<details class="participants-section">
<summary><strong>Participants</strong> <span class="participant-count">${details.participants.length} / ${details.max_participants}</span></summary>
${details.participants.length === 0 ? '<p class="no-participants">No participants yet — be the first!</p>' : ''}
</details>
`;

if (details.participants.length > 0) {
const detailsEl = activityCard.querySelector(".participants-section");
const ul = document.createElement("ul");
ul.className = "participants-list";
details.participants.forEach(p => {
const li = document.createElement("li");

const icon = document.createElement("span");
icon.className = "participant-icon";
icon.textContent = "\u{1F464}";

const emailSpan = document.createElement("span");
emailSpan.className = "participant-email";
emailSpan.textContent = p;

const deleteBtn = document.createElement("button");
deleteBtn.className = "delete-participant-btn";
deleteBtn.innerHTML = "&#128465;";
deleteBtn.title = `Unregister ${p}`;
deleteBtn.addEventListener("click", () => unregisterParticipant(name, p));

li.appendChild(icon);
li.appendChild(emailSpan);
li.appendChild(deleteBtn);
ul.appendChild(li);
});
detailsEl.appendChild(ul);
}

activitiesList.appendChild(activityCard);

// Add option to select dropdown
Expand All @@ -41,6 +74,25 @@ document.addEventListener("DOMContentLoaded", () => {
}
}

// Unregister a participant from an activity
async function unregisterParticipant(activityName, email) {
try {
const response = await fetch(
`/activities/${encodeURIComponent(activityName)}/signup?email=${encodeURIComponent(email)}`,
{ method: "DELETE" }
);
const result = await response.json();
if (response.ok) {
fetchActivities();
} else {
alert(result.detail || "Failed to unregister participant.");
}
} catch (error) {
console.error("Error unregistering participant:", error);
alert("Failed to unregister participant.");
}
}

// Handle form submission
signupForm.addEventListener("submit", async (event) => {
event.preventDefault();
Expand All @@ -62,6 +114,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";
Expand Down
118 changes: 118 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,124 @@ section h3 {
margin-bottom: 8px;
}

.spots-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.85em;
font-weight: bold;
}

.spots-ok {
background-color: #e8f5e9;
color: #2e7d32;
border: 1px solid #a5d6a7;
}

.spots-low {
background-color: #fff3e0;
color: #e65100;
border: 1px solid #ffcc80;
}

.spots-full {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ef9a9a;
}

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

.participants-section summary {
cursor: pointer;
user-select: none;
list-style: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 0;
color: #1a237e;
}

.participants-section summary::-webkit-details-marker {
display: none;
}

.participants-section summary::after {
content: '\25BC';
font-size: 0.7em;
transition: transform 0.2s;
color: #888;
}

.participants-section[open] summary::after {
transform: rotate(180deg);
}

.participant-count {
font-size: 0.8em;
background-color: #e8eaf6;
color: #3949ab;
border-radius: 10px;
padding: 1px 7px;
margin-left: 8px;
font-weight: normal;
}

.participants-list {
list-style: none;
margin-top: 10px;
padding: 0;
display: flex;
flex-direction: column;
gap: 6px;
}

.participants-list li {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.9em;
color: #444;
background-color: #f0f4ff;
border-radius: 6px;
padding: 5px 10px;
border: 1px solid #d0d8f0;
}

.participant-icon {
font-size: 1em;
}

.delete-participant-btn {
margin-left: auto;
background: none;
border: none;
cursor: pointer;
font-size: 1em;
padding: 0 2px;
color: #c62828;
opacity: 0.6;
transition: opacity 0.15s, transform 0.15s;
line-height: 1;
}

.delete-participant-btn:hover {
opacity: 1;
transform: scale(1.2);
}

.no-participants {
margin-top: 10px;
font-size: 0.85em;
color: #888;
font-style: italic;
}

.form-group {
margin-bottom: 15px;
}
Expand Down
Empty file added tests/__init__.py
Empty file.
Loading