Skip to content
Closed
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
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
fastapi
uvicorn

# Test dependencies
pytest
httpx
57 changes: 56 additions & 1 deletion src/app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

"""
High School Management System API

Expand Down Expand Up @@ -38,6 +39,45 @@
"schedule": "Mondays, Wednesdays, Fridays, 2:00 PM - 3:00 PM",
"max_participants": 30,
"participants": ["[email protected]", "[email protected]"]
},
# 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": ["[email protected]", "[email protected]"]
},
"Basketball Club": {
"description": "Pickup games and skill development for all levels",
"schedule": "Tuesdays, Thursdays, 5:00 PM - 6:30 PM",
"max_participants": 18,
"participants": ["[email protected]", "[email protected]"]
},
# Artistic activities
"Art Club": {
"description": "Explore drawing, painting, and mixed media projects",
"schedule": "Wednesdays, 3:30 PM - 5:00 PM",
"max_participants": 16,
"participants": ["[email protected]", "[email protected]"]
},
"Music Ensemble": {
"description": "Practice and perform ensemble pieces across genres",
"schedule": "Fridays, 4:00 PM - 6:00 PM",
"max_participants": 25,
"participants": ["[email protected]", "[email protected]"]
},
# 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": ["[email protected]", "[email protected]"]
},
"Robotics Club": {
"description": "Design, build, and program robots for competitions and projects",
"schedule": "Tuesdays, 4:00 PM - 6:00 PM",
"max_participants": 12,
"participants": ["[email protected]", "[email protected]"]
}
}

Expand All @@ -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}"}
120 changes: 116 additions & 4 deletions src/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 = `
<h4>${name}</h4>
<p>${details.description}</p>
<p><strong>Schedule:</strong> ${details.schedule}</p>
<p>${details.description || ""}</p>
<p><strong>Schedule:</strong> ${details.schedule || "TBA"}</p>
<p><strong>Availability:</strong> ${spotsLeft} spots left</p>
`;
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 = "&#128465;"; // 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
Expand Down Expand Up @@ -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";
Expand All @@ -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);
}
}
});
41 changes: 41 additions & 0 deletions src/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,47 @@ <h3>Sign Up for an Activity</h3>
</form>
<div id="message" class="hidden"></div>
</section>

<section id="example-activities">
<h3>Example Activities</h3>
<div class="activity-card">
<h4>Morning Hike</h4>
<p>Saturday, 8:00 AM · Easy</p>

<div class="participants" aria-label="Participants for Morning Hike">
<h5>Participants</h5>
<ul>
<li>
<span class="participant-initials">AL</span>
Alice
</li>
<li>
<img class="participant-avatar" src="https://via.placeholder.com/22" alt="Bob's avatar" />
Bob
</li>
<li>
<span class="participant-initials">CM</span>
Casey
</li>
</ul>
</div>
</div>

<div class="activity-card">
<h4>Photography Walk</h4>
<p>Sunday, 4:00 PM · Photo tips included</p>

<div class="participants" aria-label="Participants for Photography Walk">
<h5>Participants</h5>
<ul>
<li><span class="participant-initials">EM</span> Emma</li>
<li><span class="participant-initials">RK</span> Raj</li>
<li><span class="participant-initials">SO</span> Simone</li>
<li><span class="participant-initials">+3</span> More</li>
</ul>
</div>
</div>
</section>
</main>

<footer>
Expand Down
93 changes: 93 additions & 0 deletions src/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ section h3 {
border: 1px solid #ddd;
border-radius: 5px;
background-color: #f9f9f9;
padding-bottom: 18px;
}

.activity-card h4 {
Expand All @@ -74,6 +75,90 @@ section h3 {
margin-bottom: 8px;
}

.activity-card .participants ul {
.activity-card .participants ul {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0;
margin: 0;
list-style: none;
}
.activity-card .participants li {
position: relative;
}


button.delete-participant {
margin-left: 8px;
background: none;
border: none;
cursor: pointer;
color: #c62828;
font-size: 15px;
padding: 0 2px;
vertical-align: middle;
}
button.delete-participant:hover {
color: #a31515;
}
.activity-card .participants ul li::before {
display: none !important;
}

.activity-card .participants ul {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0;
margin: 0;
list-style: none;
}

.activity-card .participants li {
display: inline-flex;
align-items: center;
gap: 8px;
background: linear-gradient(180deg,#f7f9ff 0%,#eef3ff 100%);
color: #0d1b6f;
padding: 6px 10px;
border-radius: 16px;
font-size: 13px;
border: 1px solid #e0e8ff;
}

/* decorative bullet so the participants list reads as a bulleted list but keeps the pill look */
.activity-card .participants li::before {
content: "";
width: 8px;
height: 8px;
background: #1a237e;
border-radius: 50%;
display: inline-block;
margin-right: 6px;
}

.activity-card .participants img.participant-avatar {
width: 22px;
height: 22px;
border-radius: 50%;
object-fit: cover;
border: 1px solid rgba(0,0,0,0.05);
}

.activity-card .participants .participant-initials {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #dbe7ff;
color: #0d1b6f;
font-size: 12px;
font-weight: 700;
}

.form-group {
margin-bottom: 15px;
}
Expand Down Expand Up @@ -142,3 +227,11 @@ footer {
padding: 20px;
color: #666;
}

/* more compact pills on very small screens */
@media (max-width: 420px) {
.activity-card .participants li {
padding: 5px 8px;
font-size: 12px;
}
}
Loading