diff --git a/requirements.txt b/requirements.txt index 97dc7cd..3924427 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ fastapi uvicorn + +# Test dependencies +pytest +httpx diff --git a/src/app.py b/src/app.py index 4ebb1d9..e7e1a88 100644 --- a/src/app.py +++ b/src/app.py @@ -1,3 +1,4 @@ + """ High School Management System API @@ -38,6 +39,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": "Join the school soccer team for practices and matches", + "schedule": "Mondays, Wednesdays, 4:30 PM - 6:00 PM", + "max_participants": 22, + "participants": ["liam@mergington.edu", "ava@mergington.edu"] + }, + "Basketball Club": { + "description": "Pickup games and skill development for all levels", + "schedule": "Tuesdays, Thursdays, 5:00 PM - 6:30 PM", + "max_participants": 18, + "participants": ["noah@mergington.edu", "mia@mergington.edu"] + }, + # Artistic activities + "Art Club": { + "description": "Explore drawing, painting, and mixed media projects", + "schedule": "Wednesdays, 3:30 PM - 5:00 PM", + "max_participants": 16, + "participants": ["isabella@mergington.edu", "lucas@mergington.edu"] + }, + "Music Ensemble": { + "description": "Practice and perform ensemble pieces across genres", + "schedule": "Fridays, 4:00 PM - 6:00 PM", + "max_participants": 25, + "participants": ["amelia@mergington.edu", "ethan@mergington.edu"] + }, + # Intellectual activities + "Debate Team": { + "description": "Develop public speaking and argumentation skills; compete in tournaments", + "schedule": "Thursdays, 3:30 PM - 5:00 PM", + "max_participants": 14, + "participants": ["harper@mergington.edu", "jack@mergington.edu"] + }, + "Robotics Club": { + "description": "Design, build, and program robots for competitions and projects", + "schedule": "Tuesdays, 4:00 PM - 6:00 PM", + "max_participants": 12, + "participants": ["charlotte@mergington.edu", "logan@mergington.edu"] } } @@ -61,7 +101,22 @@ 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.post("/activities/{activity_name}/unregister") +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=400, detail="Student is not registered for this activity") + activity["participants"].remove(email) + return {"message": f"Removed {email} from {activity_name}"} diff --git a/src/static/app.js b/src/static/app.js index dcc1e38..b2beea3 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -4,6 +4,20 @@ document.addEventListener("DOMContentLoaded", () => { const signupForm = document.getElementById("signup-form"); const messageDiv = document.getElementById("message"); + // Function to create a display name and initials from an email or name + function nameFromIdentifier(id) { + if (!id) return { display: "Unknown", initials: "?" }; + // if it's an email, use part before @ + const raw = id.includes("@") ? id.split("@")[0] : id; + // replace dots/underscores with spaces and split to words + const parts = raw.replace(/[._\-]+/g, " ").split(" ").filter(Boolean); + const display = parts.map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(" "); + const initials = parts.length === 1 + ? parts[0].substring(0, 2).toUpperCase() + : (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); + return { display: display || raw, initials }; + } + // Function to fetch activities from API async function fetchActivities() { try { @@ -13,20 +27,90 @@ document.addEventListener("DOMContentLoaded", () => { // Clear loading message activitiesList.innerHTML = ""; + // Reset select (keep placeholder if present) + const placeholderOption = activitySelect.querySelector('option[value=""]'); + activitySelect.innerHTML = ""; + if (placeholderOption) { + activitySelect.appendChild(placeholderOption); + } else { + // ensure a default placeholder exists + const opt = document.createElement("option"); + opt.value = ""; + opt.textContent = "-- Select an activity --"; + activitySelect.appendChild(opt); + } + // 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 spotsLeft = details.max_participants - (details.participants?.length || 0); - activityCard.innerHTML = ` + // Basic info + const infoHtml = `

${name}

-

${details.description}

-

Schedule: ${details.schedule}

+

${details.description || ""}

+

Schedule: ${details.schedule || "TBA"}

Availability: ${spotsLeft} spots left

`; + activityCard.innerHTML = infoHtml; + + // Participants section + const participantsDiv = document.createElement("div"); + participantsDiv.className = "participants"; + participantsDiv.setAttribute("aria-label", `Participants for ${name}`); + + const title = document.createElement("h5"); + title.textContent = "Participants"; + participantsDiv.appendChild(title); + + const participants = details.participants || []; + + if (participants.length === 0) { + const none = document.createElement("p"); + none.className = "info"; + none.textContent = "No participants yet"; + participantsDiv.appendChild(none); + } else { + const list = document.createElement("ul"); + list.style.listStyle = "none"; + list.style.padding = "0"; + participants.forEach((p) => { + const { display, initials } = nameFromIdentifier(p); + const li = document.createElement("li"); + + const span = document.createElement("span"); + span.className = "participant-initials"; + span.textContent = initials; + + li.appendChild(span); + li.appendChild(document.createTextNode(" " + display)); + + // Add delete icon + const delBtn = document.createElement("button"); + delBtn.className = "delete-participant"; + delBtn.title = "Remove participant"; + delBtn.innerHTML = "🗑"; // trash can icon + delBtn.style.marginLeft = "8px"; + delBtn.style.background = "none"; + delBtn.style.border = "none"; + delBtn.style.cursor = "pointer"; + delBtn.style.color = "#c62828"; + delBtn.style.fontSize = "15px"; + delBtn.setAttribute("aria-label", `Remove ${display}`); + delBtn.addEventListener("click", (e) => { + e.stopPropagation(); + unregisterParticipant(name, p); + }); + li.appendChild(delBtn); + + list.appendChild(li); + }); + participantsDiv.appendChild(list); + } + activityCard.appendChild(participantsDiv); activitiesList.appendChild(activityCard); // Add option to select dropdown @@ -62,6 +146,8 @@ document.addEventListener("DOMContentLoaded", () => { messageDiv.textContent = result.message; messageDiv.className = "success"; signupForm.reset(); + // Refresh activities to show updated participants & availability + await fetchActivities(); } else { messageDiv.textContent = result.detail || "An error occurred"; messageDiv.className = "error"; @@ -83,4 +169,30 @@ document.addEventListener("DOMContentLoaded", () => { // Initialize app fetchActivities(); + // Unregister participant function + async function unregisterParticipant(activityName, participantId) { + if (!confirm("Are you sure you want to remove this participant?")) return; + try { + const response = await fetch(`/activities/${encodeURIComponent(activityName)}/unregister?email=${encodeURIComponent(participantId)}`, { + method: "POST" + }); + const result = await response.json(); + if (response.ok) { + messageDiv.textContent = result.message || "Participant removed."; + messageDiv.className = "success"; + await fetchActivities(); + } else { + messageDiv.textContent = result.detail || "Failed to remove participant."; + messageDiv.className = "error"; + } + messageDiv.classList.remove("hidden"); + setTimeout(() => messageDiv.classList.add("hidden"), 5000); + } catch (error) { + messageDiv.textContent = "Error removing participant."; + messageDiv.className = "error"; + messageDiv.classList.remove("hidden"); + setTimeout(() => messageDiv.classList.add("hidden"), 5000); + console.error("Error unregistering participant:", error); + } + } }); diff --git a/src/static/index.html b/src/static/index.html index 3074f6e..3ae8250 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -39,6 +39,47 @@

Sign Up for an Activity

+ +
+

Example Activities

+
+

Morning Hike

+

Saturday, 8:00 AM · Easy

+ +
+
Participants
+
    +
  • + AL + Alice +
  • +
  • + Bob's avatar + Bob +
  • +
  • + CM + Casey +
  • +
+
+
+ +
+

Photography Walk

+

Sunday, 4:00 PM · Photo tips included

+ +
+
Participants
+
    +
  • EM Emma
  • +
  • RK Raj
  • +
  • SO Simone
  • +
  • +3 More
  • +
+
+
+