diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..167fe26
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "chat.tools.terminal.autoApprove": {
+ "uvicorn": true
+ }
+}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 97dc7cd..2522ad0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,4 @@
fastapi
uvicorn
+pytest
+httpx
diff --git a/src/app.py b/src/app.py
index 4ebb1d9..3fc84ee 100644
--- a/src/app.py
+++ b/src/app.py
@@ -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": "Join the varsity soccer team and compete in league matches",
+ "schedule": "Mondays and Wednesdays, 4:00 PM - 6:00 PM",
+ "max_participants": 25,
+ "participants": ["liam@mergington.edu", "noah@mergington.edu"]
+ },
+ "Basketball Team": {
+ "description": "Practice basketball skills and play competitive games",
+ "schedule": "Tuesdays and Thursdays, 4:00 PM - 6:00 PM",
+ "max_participants": 15,
+ "participants": ["ava@mergington.edu", "isabella@mergington.edu"]
+ },
+ "Drama Club": {
+ "description": "Perform in theatrical productions and develop acting skills",
+ "schedule": "Thursdays, 3:30 PM - 5:30 PM",
+ "max_participants": 25,
+ "participants": ["mia@mergington.edu", "charlotte@mergington.edu"]
+ },
+ "Art Club": {
+ "description": "Explore various art mediums including painting, drawing, and sculpture",
+ "schedule": "Wednesdays, 3:30 PM - 5:00 PM",
+ "max_participants": 18,
+ "participants": ["ethan@mergington.edu", "amelia@mergington.edu"]
+ },
+ "Debate Team": {
+ "description": "Develop critical thinking and public speaking skills through competitive debates",
+ "schedule": "Tuesdays, 3:30 PM - 5:00 PM",
+ "max_participants": 16,
+ "participants": ["james@mergington.edu", "harper@mergington.edu"]
+ },
+ "Science Club": {
+ "description": "Conduct experiments and explore scientific concepts through hands-on projects",
+ "schedule": "Fridays, 3:30 PM - 5:00 PM",
+ "max_participants": 20,
+ "participants": ["benjamin@mergington.edu", "evelyn@mergington.edu"]
}
}
@@ -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}/unregister")
+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 not signed up for this activity")
+
+ # Remove student
+ activity["participants"].remove(email)
+ return {"message": f"Unregistered {email} from {activity_name}"}
diff --git a/src/static/app.js b/src/static/app.js
index dcc1e38..b781e29 100644
--- a/src/static/app.js
+++ b/src/static/app.js
@@ -12,6 +12,9 @@ document.addEventListener("DOMContentLoaded", () => {
// Clear loading message
activitiesList.innerHTML = "";
+
+ // Clear and reset activity dropdown
+ activitySelect.innerHTML = '';
// Populate activities list
Object.entries(activities).forEach(([name, details]) => {
@@ -25,10 +28,41 @@ document.addEventListener("DOMContentLoaded", () => {
${details.description}
Schedule: ${details.schedule}
Availability: ${spotsLeft} spots left
+
+
Participants:
+ ${details.participants.length > 0
+ ? `
+ ${details.participants.map(email => `- ${email}
`).join('')}
+
`
+ : `
No participants yet. Be the first to sign up!
`
+ }
+
`;
activitiesList.appendChild(activityCard);
+ // Add event listeners for delete buttons
+ activityCard.querySelectorAll('.delete-btn').forEach(btn => {
+ btn.addEventListener('click', async () => {
+ const activity = btn.dataset.activity;
+ const email = btn.dataset.email;
+ try {
+ const response = await fetch(`/activities/${encodeURIComponent(activity)}/unregister?email=${encodeURIComponent(email)}`, {
+ method: 'DELETE'
+ });
+ const result = await response.json();
+ if (response.ok) {
+ fetchActivities(); // Refresh activities list
+ } else {
+ alert(result.detail || 'An error occurred');
+ }
+ } catch (error) {
+ alert('Failed to unregister. Please try again.');
+ console.error('Error unregistering:', error);
+ }
+ });
+ });
+
// Add option to select dropdown
const option = document.createElement("option");
option.value = name;
@@ -62,6 +96,9 @@ document.addEventListener("DOMContentLoaded", () => {
messageDiv.textContent = result.message;
messageDiv.className = "success";
signupForm.reset();
+
+ // Refresh activities list to show updated participants
+ fetchActivities();
} else {
messageDiv.textContent = result.detail || "An error occurred";
messageDiv.className = "error";
diff --git a/src/static/styles.css b/src/static/styles.css
index a533b32..93cf129 100644
--- a/src/static/styles.css
+++ b/src/static/styles.css
@@ -74,6 +74,54 @@ section h3 {
margin-bottom: 8px;
}
+.participants-section {
+ margin-top: 15px;
+ padding-top: 15px;
+ border-top: 1px solid #e0e0e0;
+}
+
+.participants-section strong {
+ display: block;
+ margin-bottom: 8px;
+ color: #1a237e;
+}
+
+.participants-list {
+ list-style-type: none;
+ padding-left: 0;
+ margin: 0;
+}
+
+.participants-list li {
+ padding: 3px 0;
+ color: #555;
+ font-size: 14px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.delete-btn {
+ background: none;
+ border: none;
+ color: #d32f2f;
+ cursor: pointer;
+ font-size: 16px;
+ font-weight: bold;
+ padding: 0 5px;
+}
+
+.delete-btn:hover {
+ color: #b71c1c;
+}
+
+.no-participants {
+ font-style: italic;
+ color: #999;
+ margin: 0;
+ font-size: 14px;
+}
+
.form-group {
margin-bottom: 15px;
}
diff --git a/tests/test_app.py b/tests/test_app.py
new file mode 100644
index 0000000..e85c128
--- /dev/null
+++ b/tests/test_app.py
@@ -0,0 +1,73 @@
+import pytest
+from httpx import AsyncClient
+from src.app import app
+
+@pytest.mark.asyncio
+async def test_get_activities():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ response = await client.get("/activities")
+ assert response.status_code == 200
+ data = response.json()
+ assert "Chess Club" in data
+ assert "participants" in data["Chess Club"]
+
+@pytest.mark.asyncio
+async def test_root_redirect():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ response = await client.get("/")
+ assert response.status_code == 307 # Redirect
+ assert response.headers["location"] == "/static/index.html"
+
+@pytest.mark.asyncio
+async def test_signup_success():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ response = await client.post("/activities/Chess%20Club/signup?email=test@mergington.edu")
+ assert response.status_code == 200
+ data = response.json()
+ assert "Signed up" in data["message"]
+
+@pytest.mark.asyncio
+async def test_signup_already_signed():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ # First signup
+ await client.post("/activities/Chess%20Club/signup?email=test2@mergington.edu")
+ # Second
+ response = await client.post("/activities/Chess%20Club/signup?email=test2@mergington.edu")
+ assert response.status_code == 400
+ data = response.json()
+ assert "already signed up" in data["detail"]
+
+@pytest.mark.asyncio
+async def test_signup_activity_not_found():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ response = await client.post("/activities/NonExistent/signup?email=test@mergington.edu")
+ assert response.status_code == 404
+ data = response.json()
+ assert "Activity not found" in data["detail"]
+
+@pytest.mark.asyncio
+async def test_unregister_success():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ # Signup first
+ await client.post("/activities/Chess%20Club/signup?email=unreg@mergington.edu")
+ # Unregister
+ response = await client.delete("/activities/Chess%20Club/unregister?email=unreg@mergington.edu")
+ assert response.status_code == 200
+ data = response.json()
+ assert "Unregistered" in data["message"]
+
+@pytest.mark.asyncio
+async def test_unregister_not_signed():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ response = await client.delete("/activities/Chess%20Club/unregister?email=notsigned@mergington.edu")
+ assert response.status_code == 400
+ data = response.json()
+ assert "not signed up" in data["detail"]
+
+@pytest.mark.asyncio
+async def test_unregister_activity_not_found():
+ async with AsyncClient(app=app, base_url="http://testserver") as client:
+ response = await client.delete("/activities/NonExistent/unregister?email=test@mergington.edu")
+ assert response.status_code == 404
+ data = response.json()
+ assert "Activity not found" in data["detail"]
\ No newline at end of file