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: 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
59 changes: 59 additions & 0 deletions src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,47 @@
"max_participants": 20,
"participants": ["emma@mergington.edu", "sophia@mergington.edu"]
},
"Debate Team": {
"description": "Develop critical thinking and public speaking skills through competitive debate",
"schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM",
"max_participants": 15,
"participants": ["alex@mergington.edu"]
},
"Robotics Club": {
"description": "Design, build, and program robots for competitions",
"schedule": "Tuesdays, Wednesdays, Thursdays, 3:30 PM - 5:00 PM",
"max_participants": 25,
"participants": ["james@mergington.edu", "sarah@mergington.edu"]
},
"Gym Class": {
"description": "Physical education and sports activities",
"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 training and games",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 20,
"participants": ["marcus@mergington.edu", "lucas@mergington.edu"]
},
"Track and Field": {
"description": "Training for running, jumping, and throwing events",
"schedule": "Mondays, Wednesdays, Fridays, 3:30 PM - 5:00 PM",
"max_participants": 25,
"participants": ["ava@mergington.edu"]
},
"Art Club": {
"description": "Explore painting, drawing, and sculpture with expert instructors",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 18,
"participants": ["isabella@mergington.edu", "grace@mergington.edu"]
},
"Music Ensemble": {
"description": "Join our orchestra, band, or choir and perform at school events",
"schedule": "Tuesdays and Fridays, 3:30 PM - 4:30 PM",
"max_participants": 40,
"participants": ["noah@mergington.edu", "mia@mergington.edu", "lucas@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")

# 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 currently signed up
if email not in activity["participants"]:
raise HTTPException(status_code=404, detail="Student is not signed up for this activity")

# Remove student
activity["participants"].remove(email)
return {"message": f"Unregistered {email} from {activity_name}"}
77 changes: 63 additions & 14 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,53 @@ document.addEventListener("DOMContentLoaded", () => {
const signupForm = document.getElementById("signup-form");
const messageDiv = document.getElementById("message");

function showMessage(text, type) {
messageDiv.textContent = text;
messageDiv.className = type;
messageDiv.classList.remove("hidden");

// Hide message after 5 seconds
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
}
Comment on lines +7 to +16

// Function to fetch activities from API
async function fetchActivities() {
try {
const response = await fetch("/activities");
// Add a cache-busting query and disable cache to always show latest participant updates.
const response = await fetch(`/activities?ts=${Date.now()}`, { cache: "no-store" });
const activities = await response.json();

// Clear loading message
activitiesList.innerHTML = "";
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 spotsLeft = details.max_participants - details.participants.length;
const participantsMarkup =
details.participants.length > 0
? `<ul class="participants-list">${details.participants
.map(
(participant) =>
`<li><span class="participant-email">${participant}</span><button type="button" class="participant-delete" data-activity="${encodeURIComponent(name)}" data-email="${encodeURIComponent(participant)}" aria-label="Unregister ${participant}" title="Unregister participant">x</button></li>`
)
.join("")}</ul>`
: '<p class="participants-empty">No participants yet.</p>';
Comment on lines +35 to +43

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

activitiesList.appendChild(activityCard);
Expand Down Expand Up @@ -53,31 +79,54 @@ document.addEventListener("DOMContentLoaded", () => {
`/activities/${encodeURIComponent(activity)}/signup?email=${encodeURIComponent(email)}`,
{
method: "POST",
cache: "no-store",
}
);

const result = await response.json();

if (response.ok) {
messageDiv.textContent = result.message;
messageDiv.className = "success";
showMessage(result.message, "success");
signupForm.reset();
await fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
showMessage(result.detail || "An error occurred", "error");
}
} catch (error) {
showMessage("Failed to sign up. Please try again.", "error");
console.error("Error signing up:", error);
}
});

activitiesList.addEventListener("click", async (event) => {
const deleteButton = event.target.closest(".participant-delete");
if (!deleteButton) {
return;
}

messageDiv.classList.remove("hidden");
const activityName = decodeURIComponent(deleteButton.dataset.activity || "");
const email = decodeURIComponent(deleteButton.dataset.email || "");

// Hide message after 5 seconds
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
try {
const response = await fetch(
`/activities/${encodeURIComponent(activityName)}/signup?email=${encodeURIComponent(email)}`,
{
method: "DELETE",
cache: "no-store",
}
);

const result = await response.json();

if (response.ok) {
showMessage(result.message, "success");
await fetchActivities();
} else {
showMessage(result.detail || "Failed to unregister participant.", "error");
}
} catch (error) {
messageDiv.textContent = "Failed to sign up. Please try again.";
messageDiv.className = "error";
messageDiv.classList.remove("hidden");
console.error("Error signing up:", error);
showMessage("Failed to unregister participant. Please try again.", "error");
console.error("Error unregistering participant:", error);
}
});

Expand Down
61 changes: 61 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,67 @@ section h3 {
margin-bottom: 8px;
}

.participants-section {
margin-top: 12px;
padding: 12px;
background: linear-gradient(135deg, #eef4ff 0%, #f7fbff 100%);
border: 1px solid #d8e6ff;
border-radius: 8px;
}

.participants-title {
margin-bottom: 8px;
color: #1a237e;
letter-spacing: 0.2px;
}

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

.participants-list li {
margin-bottom: 6px;
color: #29406b;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}

.participants-list li:last-child {
margin-bottom: 0;
}

.participant-email {
overflow-wrap: anywhere;
}

.participant-delete {
width: 24px;
height: 24px;
border-radius: 50%;
border: 1px solid #d95757;
background-color: #fff;
color: #c62828;
font-weight: bold;
line-height: 1;
cursor: pointer;
padding: 0;
transition: background-color 0.2s ease, color 0.2s ease;
}

.participant-delete:hover {
background-color: #ffebee;
}

.participants-empty {
margin: 0;
color: #5f6b7a;
font-style: italic;
}

.form-group {
margin-bottom: 15px;
}
Expand Down
Loading
Loading