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
55 changes: 55 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"]
},
"Basketball Team": {
"description": "Competitive basketball team for interscholastic games",
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:30 PM",
"max_participants": 15,
"participants": ["alex@mergington.edu", "jordan@mergington.edu"]
},
"Tennis Club": {
"description": "Learn tennis skills and compete in matches",
"schedule": "Mondays and Wednesdays, 4:00 PM - 5:00 PM",
"max_participants": 16,
"participants": ["sarah@mergington.edu", "james@mergington.edu"]
},
"Drama Club": {
"description": "Perform in theatrical productions and develop acting skills",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 25,
"participants": ["lisa@mergington.edu", "david@mergington.edu"]
},
"Art Class": {
"description": "Explore painting, sculpture, and other visual arts",
"schedule": "Fridays, 4:00 PM - 5:30 PM",
"max_participants": 18,
"participants": ["maya@mergington.edu", "chris@mergington.edu"]
},
"Science Club": {
"description": "Conduct experiments and explore scientific concepts",
"schedule": "Thursdays, 3:30 PM - 4:45 PM",
"max_participants": 20,
"participants": ["ryan@mergington.edu", "natalie@mergington.edu"]
},
"Debate Team": {
"description": "Develop public speaking and critical thinking skills through competitive debate",
"schedule": "Mondays, 3:30 PM - 5:00 PM",
"max_participants": 14,
"participants": ["tyler@mergington.edu", "jessica@mergington.edu"]
}
}

Expand All @@ -62,6 +98,25 @@ 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_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 activity")

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

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

setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
}

// Function to fetch activities from API
async function fetchActivities() {
try {
const response = await fetch("/activities");
const activities = await response.json();

// Clear loading message
// Clear loading message and reset activity dropdown
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 participants = details.participants || [];

activityCard.innerHTML = `
<h4>${name}</h4>
Expand All @@ -27,6 +39,51 @@ document.addEventListener("DOMContentLoaded", () => {
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
`;

const participantsSection = document.createElement("div");
participantsSection.className = "participants-section";

const participantsTitle = document.createElement("p");
participantsTitle.innerHTML = "<strong>Participants:</strong>";
participantsSection.appendChild(participantsTitle);

if (participants.length) {
const participantsList = document.createElement("ul");
participantsList.className = "participants-list";

participants.forEach((participant) => {
const listItem = document.createElement("li");
listItem.className = "participant-item";

const nameSpan = document.createElement("span");
nameSpan.className = "participant-name";
nameSpan.textContent = participant;

const removeButton = document.createElement("button");
removeButton.type = "button";
removeButton.className = "remove-participant";
removeButton.dataset.activity = name;
removeButton.dataset.email = participant;
removeButton.title = `Remove ${participant}`;
removeButton.innerHTML = `
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M9 3V4H4V6H5V19C5 20.1 5.9 21 7 21H17C18.1 21 19 20.1 19 19V6H20V4H15V3H9ZM7 6H17V19H7V6ZM9 8V17H11V8H9ZM13 8V17H15V8H13Z" />
</svg>
`;

listItem.appendChild(nameSpan);
listItem.appendChild(removeButton);
participantsList.appendChild(listItem);
});

participantsSection.appendChild(participantsList);
} else {
const emptyMessage = document.createElement("p");
emptyMessage.className = "no-participants";
emptyMessage.textContent = "No participants yet. Be the first to sign up!";
participantsSection.appendChild(emptyMessage);
}

activityCard.appendChild(participantsSection);
activitiesList.appendChild(activityCard);

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

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

const activity = removeButton.dataset.activity;
const email = removeButton.dataset.email;

if (!activity || !email) return;

try {
const response = await fetch(
`/activities/${encodeURIComponent(activity)}/participants?email=${encodeURIComponent(email)}`,
{
method: "DELETE",
}
);
const result = await response.json();

if (response.ok) {
showMessage(result.message, "success");
fetchActivities();
} else {
showMessage(result.detail || "An error occurred", "error");
}
} catch (error) {
showMessage("Failed to unregister participant. Please try again.", "error");
console.error("Error removing participant:", error);
}
});

// Handle form submission
signupForm.addEventListener("submit", async (event) => {
event.preventDefault();
Expand All @@ -59,20 +146,12 @@ document.addEventListener("DOMContentLoaded", () => {
const result = await response.json();

if (response.ok) {
messageDiv.textContent = result.message;
messageDiv.className = "success";
showMessage(result.message, "success");
signupForm.reset();
fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
showMessage(result.detail || "An error occurred", "error");
}

messageDiv.classList.remove("hidden");

// Hide message after 5 seconds
setTimeout(() => {
messageDiv.classList.add("hidden");
}, 5000);
} catch (error) {
messageDiv.textContent = "Failed to sign up. Please try again.";
messageDiv.className = "error";
Expand Down
67 changes: 67 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,73 @@ section h3 {
margin-bottom: 8px;
}

.participants-section {
margin-top: 15px;
padding: 16px 14px 12px;
border-top: 1px solid #e0e0e0;
background-color: #fafbff;
border-radius: 0 0 5px 5px;
}

.participants-section p {
margin-bottom: 10px;
}

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

.participant-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
margin-bottom: 10px;
background-color: #eef1ff;
border: 1px solid #d6dbff;
border-radius: 999px;
}

.participant-name {
color: #1f2a55;
font-size: 0.95rem;
word-break: break-word;
}

.remove-participant {
border: none;
background: transparent;
color: #c62828;
width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.2s ease;
padding: 0;
}

.remove-participant svg {
width: 18px;
height: 18px;
fill: currentColor;
}

.remove-participant:hover {
background-color: rgba(198, 40, 40, 0.12);
}

.no-participants {
margin: 0;
font-style: italic;
color: #666;
}

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