Skip to content

Commit 7ae3d32

Browse files
authored
Merge pull request #2 from terioki/accelerate-with-copilot
Improve student activity registration system
2 parents 65e8686 + 722eaa3 commit 7ae3d32

6 files changed

Lines changed: 471 additions & 17 deletions

File tree

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
fastapi
22
uvicorn
33
httpx
4-
watchfiles
4+
watchfiles
5+
pytest

src/app.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,42 @@
3838
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
3939
"max_participants": 30,
4040
"participants": ["john@mergington.edu", "olivia@mergington.edu"]
41+
},
42+
"Basketball Team": {
43+
"description": "Competitive basketball team for intramural and league play",
44+
"schedule": "Mondays and Wednesdays, 4:00 PM - 5:30 PM",
45+
"max_participants": 15,
46+
"participants": ["alex@mergington.edu"]
47+
},
48+
"Tennis Club": {
49+
"description": "Learn tennis skills and compete in tournaments",
50+
"schedule": "Tuesdays and Thursdays, 4:00 PM - 5:00 PM",
51+
"max_participants": 10,
52+
"participants": ["lucas@mergington.edu", "grace@mergington.edu"]
53+
},
54+
"Drama Club": {
55+
"description": "Perform in theatrical productions and develop acting skills",
56+
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
57+
"max_participants": 25,
58+
"participants": ["maya@mergington.edu", "james@mergington.edu"]
59+
},
60+
"Art Studio": {
61+
"description": "Explore painting, drawing, and sculpture techniques",
62+
"schedule": "Thursdays, 3:30 PM - 5:00 PM",
63+
"max_participants": 18,
64+
"participants": ["isabella@mergington.edu"]
65+
},
66+
"Debate Team": {
67+
"description": "Develop public speaking and critical thinking skills through debate",
68+
"schedule": "Mondays and Fridays, 3:30 PM - 4:30 PM",
69+
"max_participants": 16,
70+
"participants": ["ethan@mergington.edu", "ava@mergington.edu"]
71+
},
72+
"Science Club": {
73+
"description": "Conduct experiments and explore advanced scientific concepts",
74+
"schedule": "Tuesdays, 3:30 PM - 5:00 PM",
75+
"max_participants": 20,
76+
"participants": ["noah@mergington.edu"]
4177
}
4278
}
4379

@@ -62,6 +98,25 @@ def signup_for_activity(activity_name: str, email: str):
6298
# Get the specific activity
6399
activity = activities[activity_name]
64100

101+
# Validate student is not already signed up
102+
if email in activity["participants"]:
103+
raise HTTPException(status_code=400, detail="Student already signed up for this activity")
104+
65105
# Add student
66106
activity["participants"].append(email)
67107
return {"message": f"Signed up {email} for {activity_name}"}
108+
109+
110+
@app.delete("/activities/{activity_name}/participants")
111+
def unregister_from_activity(activity_name: str, email: str):
112+
"""Remove a student from an activity"""
113+
if activity_name not in activities:
114+
raise HTTPException(status_code=404, detail="Activity not found")
115+
116+
activity = activities[activity_name]
117+
118+
if email not in activity["participants"]:
119+
raise HTTPException(status_code=404, detail="Participant not found for this activity")
120+
121+
activity["participants"].remove(email)
122+
return {"message": f"Removed {email} from {activity_name}"}

src/static/app.js

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ document.addEventListener("DOMContentLoaded", () => {
44
const signupForm = document.getElementById("signup-form");
55
const messageDiv = document.getElementById("message");
66

7+
function showMessage(text, type) {
8+
messageDiv.textContent = text;
9+
messageDiv.className = type;
10+
messageDiv.classList.remove("hidden");
11+
12+
setTimeout(() => {
13+
messageDiv.classList.add("hidden");
14+
}, 5000);
15+
}
16+
717
// Function to fetch activities from API
818
async function fetchActivities() {
919
try {
@@ -12,19 +22,46 @@ document.addEventListener("DOMContentLoaded", () => {
1222

1323
// Clear loading message
1424
activitiesList.innerHTML = "";
25+
activitySelect.innerHTML = '<option value="">-- Select an activity --</option>';
1526

1627
// Populate activities list
1728
Object.entries(activities).forEach(([name, details]) => {
1829
const activityCard = document.createElement("div");
1930
activityCard.className = "activity-card";
2031

2132
const spotsLeft = details.max_participants - details.participants.length;
33+
const participantsMarkup = details.participants.length
34+
? details.participants
35+
.map(
36+
(participant) => `
37+
<li class="participant-item">
38+
<span class="participant-email">${participant}</span>
39+
<button
40+
class="participant-delete-button"
41+
type="button"
42+
data-activity="${encodeURIComponent(name)}"
43+
data-email="${encodeURIComponent(participant)}"
44+
aria-label="Unregister ${participant} from ${name}"
45+
>
46+
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
47+
<path d="M9 3h6l1 2h4v2H4V5h4l1-2zm1 6h2v8h-2V9zm4 0h2v8h-2V9zM7 9h2v8H7V9zm-1 11h12l1-12H5l1 12z" />
48+
</svg>
49+
</button>
50+
</li>
51+
`
52+
)
53+
.join("")
54+
: '<li class="participant-empty">No students registered yet.</li>';
2255

2356
activityCard.innerHTML = `
2457
<h4>${name}</h4>
2558
<p>${details.description}</p>
2659
<p><strong>Schedule:</strong> ${details.schedule}</p>
2760
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
61+
<div class="participants-section">
62+
<p class="participants-title">Participants</p>
63+
<ul class="participants-list">${participantsMarkup}</ul>
64+
</div>
2865
`;
2966

3067
activitiesList.appendChild(activityCard);
@@ -59,25 +96,47 @@ document.addEventListener("DOMContentLoaded", () => {
5996
const result = await response.json();
6097

6198
if (response.ok) {
62-
messageDiv.textContent = result.message;
63-
messageDiv.className = "success";
6499
signupForm.reset();
100+
await fetchActivities();
101+
showMessage(result.message, "success");
65102
} else {
66-
messageDiv.textContent = result.detail || "An error occurred";
67-
messageDiv.className = "error";
103+
showMessage(result.detail || "An error occurred", "error");
68104
}
105+
} catch (error) {
106+
showMessage("Failed to sign up. Please try again.", "error");
107+
console.error("Error signing up:", error);
108+
}
109+
});
110+
111+
activitiesList.addEventListener("click", async (event) => {
112+
const deleteButton = event.target.closest(".participant-delete-button");
113+
114+
if (!deleteButton) {
115+
return;
116+
}
117+
118+
const activity = decodeURIComponent(deleteButton.dataset.activity);
119+
const email = decodeURIComponent(deleteButton.dataset.email);
69120

70-
messageDiv.classList.remove("hidden");
121+
try {
122+
const response = await fetch(
123+
`/activities/${encodeURIComponent(activity)}/participants?email=${encodeURIComponent(email)}`,
124+
{
125+
method: "DELETE",
126+
}
127+
);
71128

72-
// Hide message after 5 seconds
73-
setTimeout(() => {
74-
messageDiv.classList.add("hidden");
75-
}, 5000);
129+
const result = await response.json();
130+
131+
if (response.ok) {
132+
await fetchActivities();
133+
showMessage(result.message, "success");
134+
} else {
135+
showMessage(result.detail || "Unable to unregister participant.", "error");
136+
}
76137
} catch (error) {
77-
messageDiv.textContent = "Failed to sign up. Please try again.";
78-
messageDiv.className = "error";
79-
messageDiv.classList.remove("hidden");
80-
console.error("Error signing up:", error);
138+
showMessage("Failed to unregister participant. Please try again.", "error");
139+
console.error("Error unregistering participant:", error);
81140
}
82141
});
83142

src/static/styles.css

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ section h3 {
6060
.activity-card {
6161
margin-bottom: 15px;
6262
padding: 15px;
63-
border: 1px solid #ddd;
64-
border-radius: 5px;
65-
background-color: #f9f9f9;
63+
border: 1px solid #d6def5;
64+
border-radius: 14px;
65+
background: linear-gradient(180deg, #ffffff 0%, #f5f8ff 100%);
66+
box-shadow: 0 10px 24px rgba(26, 35, 126, 0.08);
6667
}
6768

6869
.activity-card h4 {
@@ -74,6 +75,79 @@ section h3 {
7475
margin-bottom: 8px;
7576
}
7677

78+
.participants-section {
79+
margin-top: 14px;
80+
padding-top: 12px;
81+
border-top: 1px solid #dbe4ff;
82+
}
83+
84+
.participants-title {
85+
margin-bottom: 8px;
86+
font-size: 0.95rem;
87+
font-weight: bold;
88+
text-transform: uppercase;
89+
letter-spacing: 0.04em;
90+
color: #1a237e;
91+
}
92+
93+
.participants-list {
94+
list-style: none;
95+
margin: 0;
96+
padding: 0;
97+
color: #334155;
98+
}
99+
100+
.participant-item {
101+
display: flex;
102+
align-items: center;
103+
justify-content: space-between;
104+
gap: 12px;
105+
margin-bottom: 8px;
106+
padding: 10px 12px;
107+
border-radius: 10px;
108+
background-color: rgba(219, 228, 255, 0.55);
109+
}
110+
111+
.participant-email {
112+
overflow-wrap: anywhere;
113+
font-weight: 600;
114+
}
115+
116+
.participant-delete-button {
117+
display: inline-flex;
118+
align-items: center;
119+
justify-content: center;
120+
width: 34px;
121+
height: 34px;
122+
flex-shrink: 0;
123+
border-radius: 999px;
124+
border: 1px solid #f3b2b2;
125+
background: linear-gradient(180deg, #fff5f5 0%, #ffe3e3 100%);
126+
color: #b42318;
127+
cursor: pointer;
128+
transition: transform 0.2s, box-shadow 0.2s, background 0.2s;
129+
}
130+
131+
.participant-delete-button:hover {
132+
transform: translateY(-1px);
133+
box-shadow: 0 6px 14px rgba(180, 35, 24, 0.18);
134+
background: linear-gradient(180deg, #ffe3e3 0%, #ffc9c9 100%);
135+
}
136+
137+
.participant-delete-button svg {
138+
width: 16px;
139+
height: 16px;
140+
fill: currentColor;
141+
}
142+
143+
.participant-empty {
144+
padding: 10px 12px;
145+
border-radius: 10px;
146+
background-color: rgba(219, 228, 255, 0.35);
147+
color: #64748b;
148+
font-style: italic;
149+
}
150+
77151
.form-group {
78152
margin-bottom: 15px;
79153
}

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)