[READ-ONLY] Mirror of https://github.com/danielroe/skills-introduction-to-repository-management. Exercise: introduction to repository management
0

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

author
Daniel Roe
committer
GitHub
date (May 8, 2025, 4:15 PM +0100) commit 9fa7b0f7
+3845
+16
.devcontainer/devcontainer.json
··· 1 + { 2 + "name": "Python 3", 3 + "image": "mcr.microsoft.com/vscode/devcontainers/python:3.13", 4 + "forwardPorts": [8000], 5 + "postCreateCommand": "bash ./.devcontainer/postCreate.sh", 6 + "customizations": { 7 + "vscode": { 8 + "extensions": [ 9 + "GitHub.copilot", 10 + "ms-python.python", 11 + "ms-python.debugpy", 12 + "mongodb.mongodb-vscode" 13 + ] 14 + } 15 + } 16 + }
+21
.devcontainer/installMongoDB.sh
··· 1 + #!/bin/bash 2 + 3 + # Install MongoDB 4 + wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | sudo apt-key add - 5 + echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list 6 + sudo apt-get update 7 + sudo apt-get install -y mongodb-org 8 + 9 + # Create necessary directories and set permissions 10 + sudo mkdir -p /data/db 11 + sudo chown -R mongodb:mongodb /data/db 12 + 13 + # Start MongoDB service 14 + sudo mongod --fork --logpath /var/log/mongodb/mongod.log 15 + 16 + echo "MongoDB has been installed and started successfully!" 17 + mongod --version 18 + 19 + # Run sample MongoDB commands 20 + echo "Current databases:" 21 + mongosh --eval "db.getMongo().getDBNames()"
+5
.devcontainer/postCreate.sh
··· 1 + # Prepare python environment 2 + pip install -r requirements.txt 3 + 4 + # Prepare MongoDB Dev DB 5 + ./.devcontainer/installMongoDB.sh
+173
.github/steps/1-protect-your-code.md
··· 1 + # Step 1: Protect your code 2 + 3 + It's been a busy month at Mergington High! Your simple website for managing extra-curricular activities has really taken off. What started as a basic sign-up form for a few activities has grown into the go-to place for half the school activities. 📚✨ 4 + 5 + Principal Martinez was so impressed with your work that they announced at the last staff meeting that ALL clubs should start using the website. While this is exciting, you're a bit nervous - the last thing you want is an accidental change breaking the system right before the big Fall Activities Fair! 😰 6 + 7 + When more teachers start helping with the Mergington High activities website, it's important to add some safeguards. Thankfully, GitHub provides several ways to protect your repository: 8 + 9 + 1. **Repository Rulesets** - These provide safeguards to limit: 10 + 11 + - Pushing code directly to important branches 12 + - Deleting or renaming branches 13 + - Force pushing (which can overwrite history) 14 + - (and much more) 15 + 16 + 1. **`.gitignore`** - This special file tells Git which files it should NOT track, like: 17 + 18 + - Temporary files that your code creates while running 19 + - Secret configuration files with sensitive information 20 + - System files that other developers don't need 21 + 22 + > [!TIP] 23 + > Think of these settings like the editorial process of a school yearbook. Various student committees will take photos and write articles, then the yearbook president will make adjustments to make sure everything flows together properly. Finally, a teacher/advisor will sign off that all content is appropriate. 24 + 25 + ## ⌨️ Activity: (optional) Get to know our extracurricular activities site 26 + 27 + <details> 28 + <summary>Show Steps</summary> 29 + 30 + In other exercise, we have been developing the Extracurricular activities website. You can follow these steps to start up the development environment and try it out. 31 + 32 + > ! **Important:** Opening a development environment and running the application is not necessary to complete this exercise. You can skip this activity if desired. 33 + 34 + 1. Right-click the below button to open the **Create Codespace** page in a new tab. Use the default configuration. 35 + 36 + [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/{{full_repo_name}}?quickstart=1) 37 + 38 + 1. Wait some time for the environment to be prepared. It will automatically install all requirements and services. 39 + 40 + 1. Validate the **GitHub Copilot** and **Python** extensions are installed and enabled. 41 + 42 + <img width="300" alt="copilot extension for VS Code" src="https://github.com/user-attachments/assets/ef1ef984-17fc-4b20-a9a6-65a866def468" /><br/> 43 + <img width="300" alt="python extension for VS Code" src="https://github.com/user-attachments/assets/3040c0f5-1658-47e2-a439-20504a384f77" /> 44 + 45 + 1. Try running the the application. In the left sidebar, select the **Run and Debug** tab and then press the **Start Debugging** icon. 46 + 47 + <details> 48 + <summary>📸 Show screenshot</summary><br/> 49 + 50 + <img width="300" alt="run and debug" src="https://github.com/user-attachments/assets/50b27f2a-5eab-4827-9343-ab5bce62357e" /> 51 + 52 + </details> 53 + 54 + <details> 55 + <summary>🤷 Having trouble?</summary><br/> 56 + 57 + If the **Run and Debug** area is empty, try reloading VS Code: Open the command palette (`Ctrl`+`Shift`+`P`) and search for `Developer: Reload Window`. 58 + 59 + <img width="300" alt="empty run and debug panel" src="https://github.com/user-attachments/assets/0dbf1407-3a97-401a-a630-f462697082d6" /> 60 + 61 + </details> 62 + 63 + 1. Use the **Ports** tab to find the webpage address, open it, and verify it is running. 64 + 65 + <details> 66 + <summary>📸 Show screenshot</summary><br/> 67 + 68 + <img width="350" alt="ports tab" src="https://github.com/user-attachments/assets/8d24d6b5-202d-4109-8174-2f0d1e4d8d44" /> 69 + 70 + ![Screenshot of Mergington High School WebApp](https://github.com/user-attachments/assets/5e1e7c1e-1b0e-4378-a5af-a266763e6544) 71 + 72 + </details> 73 + 74 + </details> 75 + 76 + ## ⌨️ Activity: Add a branch ruleset 77 + 78 + To get started, let's add some protections so that no one accidentally breaks the club registration system. 79 + 80 + 1. If necessary, open another tab and navigate to this repository. We will start on the **Settings** tab. 81 + 82 + 1. In the left navigation, expand the **Rules** area and select **Rulesets**. 83 + 84 + 1. Click the **New ruleset** dropdown and select **New branch ruleset**. 85 + 86 + <img width="250" alt="image" src="https://github.com/user-attachments/assets/1e9fd519-1421-4d6b-b654-a3fe53a8fb75" /> 87 + 88 + 1. Set the **Ruleset Name** as `Protect main` and change the **Enforcement status** to `Active`. 89 + 90 + <img width="250" alt="image" src="https://github.com/user-attachments/assets/ce30fd34-39b5-4e22-b348-4af61fd05cd1" /> 91 + 92 + 1. Find the the **Targets** section and use the **Add target** dropdown to add 2 entries: 93 + 94 + 1. Add the **Include default branch** option to ensure protections aren't bypassed by switching the default branch. 95 + 96 + <img width="250" alt="image" src="https://github.com/user-attachments/assets/217263cc-d5c2-4ac0-b03c-a72494e5c812" /> 97 + 98 + 1. Use the **include by pattern** option and enter the pattern `main`. 99 + 100 + <img width="250" alt="image" src="https://github.com/user-attachments/assets/968c9ed8-b051-44eb-af42-d99670ad31fd" /> 101 + 102 + <img width="250" alt="image" src="https://github.com/user-attachments/assets/ddc52767-d93e-4c9e-a77a-90c3b5c08fb5" /> 103 + 104 + 1. Find the **Rules** section and ensure the following items are checked. 105 + 106 + - [x] Restrict deletions 107 + - [x] Require a pull request before merging 108 + - Required approvals: `0` 109 + - [x] Require review from Code Owners 110 + - [x] Block force pushes 111 + 112 + 1. Scroll to the bottom and click the **Create** button to save the ruleset. 113 + 114 + ## ⌨️ Activity: Create a `.gitignore` file 115 + 116 + We know many teachers use different tools, so let's make sure they don't accidentally commit unnecessary files. 117 + 118 + 1. At the top navigation, return to the **Code** tab ane verify you are on the `main` branch. 119 + 120 + 1. Above the list of files, click the **Add file** dropdown and select **Create new file**. 121 + 122 + <img width="300" alt="New file button" src="https://github.com/user-attachments/assets/8f3f8da8-1471-485a-9df5-8c03ecba2d8e"/> 123 + 124 + 1. Enter the file name `.gitignore`. We will ignore the template selector for now and make our own. Copy the below example content into it. 125 + 126 + <img width="350" alt="preview of new file" src="https://github.com/user-attachments/assets/580d1a63-a264-4d44-8901-50ad708b8822"/> 127 + 128 + ```gitignore 129 + # Python backend for club management 130 + __pycache__/ 131 + *.py[cod] # Python compiled files 132 + *$py.class 133 + *.so 134 + .Python 135 + env/ 136 + .env # Where database passwords are stored 137 + venv/ # Virtual environment for testing 138 + .venv 139 + 140 + # Teacher IDE settings 141 + .vscode/ # Ms. Rodriguez uses VS Code 142 + .idea/ # Mr. Chen uses PyCharm 143 + 144 + # Local development & testing 145 + instance/ 146 + .pytest_cache/ 147 + .coverage # Test coverage reports 148 + htmlcov/ 149 + 150 + # Staff computer files 151 + .DS_Store # For teachers with Macs 152 + Thumbs.db # For teachers with Windows 153 + ``` 154 + 155 + 1. In the top right, select the **Commit changes...** button. Notice that it won't let us commit to the `main` branch! Our ruleset is working! Nice! 156 + 157 + <img width="400" alt="image" src="https://github.com/user-attachments/assets/4e85948d-75c8-4c13-8ddd-4707bf9b0805" /> 158 + 159 + 1. Enter `prepare-to-collaborate` for the branch name then click the **Propose changes** button. You will be forwarded to a new page to start a new pull request. 160 + 161 + 1. Set the title to `Prepare to collaborate` and click the **Create pull request** button. **Do NOT merge yet**, since we will be adding more collaboration related changes. 162 + 163 + 1. With the file committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson. 164 + 165 + > [!TIP] 166 + > GitHub and the community have built a repository with [sample `.gitignore` files](https://github.com/github/gitignore) for many situations. Make sure to check it out! 167 + 168 + <details> 169 + <summary>🤷 Having trouble?</summary><br/> 170 + 171 + Make sure you pushed the `.gitignore` file to `prepare-to-collaborate` branch. Exact naming for both matters! 172 + 173 + </details>
+134
.github/steps/2-prepare-to-collaborate.md
··· 1 + # Step 2: Prepare to collaborate 2 + 3 + Your simple school website has become quite popular! After showing it at the last staff meeting, Ms. Rodriguez from the Art Club and Mr. Chen from the Chess Club came up to you super excited. They have tons of ideas for new features! 4 + 5 + - Ms. Rodriguez wants to add a photo gallery 6 + - Mr. Chen dreams of adding a tournament bracket system for the chess/sports activities! 🎨♟️ 7 + 8 + While you're thrilled about their enthusiasm, you realize you need to set up some guidelines before letting them start changing code. The last thing you want is conflicting changes breaking the registration system right before spring break! 9 + 10 + Opening your project to other teachers at Mergington High means thinking about how everyone will collaborate together without breaking each other's code. 11 + 12 + **Collaborators** are the people you have granted write access to the project through repository settings. 13 + 14 + - Provide other people permissions to change project files while still protecting repository settings. 15 + - Personal repositories have simple permissions. Organization repositories allow flexible permissions such as read, write, maintain, and admin. 16 + 17 + To help with collaboration, GitHub provides two special files: 18 + 19 + 1. **`CONTRIBUTING.md` file** - A "How to Help" guide. Some example content: 20 + 21 + - How to prepare a developer setup of the extra-curricular activities website. 22 + - The process for suggesting changes. 23 + - The project's coding style preference, to keep things consistent. 24 + - How to ask for help when stuck. 25 + 26 + 1. **`CODEOWNERS` file** - Assign specific people or teams responsible for a portion of the project. 27 + 28 + - When someone creates a pull request, GitHub will automatically ask the right person to review it. 29 + 30 + ## ⌨️ Activity: Create a welcoming contribution guide 31 + 32 + The IT Club meeting is tomorrow, and you need to prepare for Ms. Rodriguez and Mr. Chen to join the project. Let's start a document to help them contribute effectively. 33 + 34 + 1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch. 35 + 36 + <img width="265" alt="image showing the correct branch" src="https://github.com/user-attachments/assets/bd12d0cc-0920-4158-9e96-e3a8fb994c1a" /> 37 + 38 + 1. In the top directory, create a new file called `CONTRIBUTING.md` (case sensitive). 39 + 40 + 1. Add a welcoming message. 41 + 42 + ```md 43 + # Contributing to the Mergington High Extra-Curricular Activities Website 44 + 45 + Thank you for your interest in helping improve our school's website! 46 + Whether you want to add your club's activities, fix a bug, or suggest 47 + new features, this guide will help you get started. 🎉 48 + ``` 49 + 50 + 1. Add instructions to help teachers quickly start developing. 51 + 52 + ```md 53 + ## Development Setup 54 + 55 + 1. Clone the repository to your computer. 56 + 2. Install Python requirements: `pip install -r requirements.txt`. 57 + 3. Run the development server: `python src/app.py`. 58 + 4. Visit http://localhost:8000 in your browser to see the website. 59 + 60 + ## Making Changes 61 + 62 + 1. Create a new branch for your changes. 63 + - Use descriptive names like `art-gallery-feature` or `fix-chess-signup` 64 + 2. Make your changes and test them locally with sample student data. 65 + - Use the MongoDB extension to preview the included sample date. 66 + 3. Push your branch and create a pull request. 67 + 4. Wait for review and address any feedback. 68 + 69 + ## Code Style 70 + 71 + - Follow PEP 8 for Python code (backend). 72 + - Use clear, descriptive variable names (student_name, start_time, etc.) 73 + - Add comments to describe blocks of logic. 74 + ``` 75 + 76 + 1. Add a section for getting help. 77 + 78 + ```md 79 + ## Need help or have ideas? 80 + 81 + - Check the open issues first. 82 + - If your problem is there, add a comment or up-vote. 83 + - If not there, create a new issue. Be as descriptive as possible. 84 + - Ask in our weekly IT Club office hours (Thursdays at lunch in Room 203). 85 + - For other general problems, email the tech team at techclub@mergingtonhigh.example.edu 86 + ``` 87 + 88 + 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch. 89 + 90 + ## ⌨️ Activity: Assign code ownership 91 + 92 + With others joining the fun, you want to stay involved on anything affecting architecture and core functionality. Let's assign you to the related files. 93 + 94 + 1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch. 95 + 96 + 1. In the top directory, create a new file called `CODEOWNERS` (case sensitive and no extension). 97 + 98 + 1. Add the following content: 99 + 100 + ```codeowners 101 + # Core functionality - changes here should be rare! 102 + /src/app.py @{{ login }} 103 + /src/backend/database.py @{{ login }} 104 + /src/backend/routers/auth.py @{{ login }} 105 + 106 + # The frontend will need refactored soon to be more object oriented. 107 + /src/static/ @{{ login }} 108 + ``` 109 + 110 + 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch. 111 + 112 + 1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson. 113 + 114 + ## ⌨️ Activity: (Optional) Add your first collaborator 115 + 116 + Ready to let your colleague start working on that photo gallery feature? Let's do it! 117 + 118 + > [!IMPORTANT] 119 + > This step is optional because it requires another person with a GitHub account to participate. 120 + 121 + 1. In the top navigation, select the **Settings** tab. 122 + 123 + 1. In the left navigation, select **Collaborators**. 124 + 125 + 1. Find the **Manage access** area and click the **Add people** button. 126 + 127 + <img width="350" alt="" src="https://github.com/user-attachments/assets/686c32c6-11c2-4e69-bad1-39062d5b4376" /> 128 + 129 + 1. Enter a friend/colleague's GitHub username or email then press the **Add to repository** button. 130 + 131 + <img width="350" alt="" src="https://github.com/user-attachments/assets/d0eaf344-baf0-4a9c-9291-c11e7a9fdaa3" /> 132 + 133 + > [!IMPORTANT] 134 + > Personal repositories only have one collaboration role type. A "collaborator" receives **write** permissions but NOT **admin** permissions. If you need finer permissions, consider starting a free [organization](https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations) and assigning [repository roles](https://docs.github.com/en/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/repository-roles-for-an-organization).
+131
.github/steps/3-foster-healthy-growth.md
··· 1 + # Step 3: Foster healthy growth 2 + 3 + With so many eager contributors, Principal Martinez pulled you aside after morning announcements: "Your website is becoming critical school infrastructure! We need to make sure it grows in a healthy way as more teachers join. Can you add some guidelines to keep everything organized?" 4 + 5 + As your extra-curricular activities website grows, you'll need more than just technical protections and contribution guides. You'll also have to encourage healthy and constructive communication. 6 + 7 + Let's look at a couple ways to do that: 8 + 9 + 1. **Code of Conduct** - This document sets expectations for how community members should interact. Think of it like the Student Handbook at Mergington High - it outlines respectful behavior, how to report non-technical problems, and consequences for violations. 10 + 11 + 2. **Issue Templates** - These provide structure when someone reports a problem or suggests a new feature. They can help the community effectively communicate their needs for new features and provide enough information to solve bugs. 12 + 13 + ## ⌨️ Activity: Set expectations with a Code of Conduct 14 + 15 + Let's start by establishing some community guidelines for your growing team of teacher-contributors. 16 + 17 + > [!TIP] 18 + > The [Contributor Covenant](https://www.contributor-covenant.org/) is a popular code of conduct used by many projects. 19 + 20 + 1. At the top navigation, return to the **Code** tab. Ensure you are on the `prepare-to-collaborate` branch. 21 + 22 + 1. In the top directory, create a new file called `CODE_OF_CONDUCT.md` (case sensitive). 23 + 24 + 1. Add the following content: 25 + 26 + ```markdown 27 + # Mergington High School Code of Conduct 28 + 29 + ## Our Pledge 30 + 31 + In the interest of fostering an open and welcoming environment for 32 + our school community, we as contributors and maintainers pledge to 33 + make participation in the Extra-Curricular Activities project a 34 + respectful and harassment-free experience for everyone. 35 + 36 + ## Our Standards 37 + 38 + Examples of behavior that contributes to creating a positive environment include: 39 + 40 + - Using welcoming and inclusive language 41 + - Being respectful of differing viewpoints and experiences 42 + - Gracefully accepting constructive criticism 43 + - Focusing on what is best for the students and the school community 44 + - Showing empathy towards other community members 45 + 46 + Examples of unacceptable behavior include: 47 + 48 + - The use of inappropriate language or imagery 49 + - Trolling, insulting comments, and personal attacks 50 + - Public or private harassment 51 + - Publishing others' private information without explicit permission 52 + - Other conduct which could reasonably be considered inappropriate in a school setting 53 + 54 + ## Responsibilities 55 + 56 + Project maintainers are responsible for clarifying the standards of 57 + acceptable behavior and are expected to take appropriate and fair 58 + corrective action in response to any instances of unacceptable behavior. 59 + 60 + Project maintainers have the right and responsibility to remove, edit, 61 + or reject comments, commits, code, issues, and other contributions that 62 + are not aligned to this Code of Conduct. 63 + 64 + ## Scope 65 + 66 + This Code of Conduct applies both within project spaces and in public spaces 67 + when an individual is representing the project or the school. Examples of 68 + representing the project include using an official project email address, 69 + posting via an official social media account, or acting as an appointed 70 + representative at an online or offline event. 71 + 72 + ## Enforcement 73 + 74 + Instances of abusive, harassing, or otherwise unacceptable behavior may be 75 + reported to the IT Club faculty advisor. All complaints will be reviewed and 76 + investigated promptly and fairly. 77 + 78 + Project maintainers who do not follow or enforce the Code of Conduct in good faith may 79 + face temporary or permanent repercussions as determined by the school administration. 80 + 81 + ## Attribution 82 + 83 + This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), 84 + version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 85 + ``` 86 + 87 + 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch. 88 + 89 + ## ⌨️ Activity: Communicate easier with issue templates 90 + 91 + Now let's create templates so other teachers can report bugs or request features in a standardized way. 92 + 93 + > [!TIP] 94 + > You might consider trying the public preview for [issue forms](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-issue-forms), which provide a friendlier user experience when creating issues. 95 + 96 + 1. In the top navigation, select the **Settings** tab. 97 + 98 + 1. Find the **Features** section and verify **Issues** is enabled. 99 + 100 + <img width="350" alt="" src="https://github.com/user-attachments/assets/dafb976b-4b8c-4c5e-8989-04d3e7bbe70d" /> 101 + 102 + 1. Click the **Set up templates** button to enter the issue templates editor. 103 + 104 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/bd94af1e-d564-472f-a435-f12fa1bf3b5c" /> 105 + 106 + 1. Click the **Add template** dropdown and select **Bug report**. 107 + 108 + <img width="350" alt="" src="https://github.com/user-attachments/assets/baee263d-b233-4029-b629-9544eacf1e27" /> 109 + 110 + 1. Click the **Preview and edit** button to show the current template. Click the **Edit icon** (pencil) to make the fields editable. 111 + 112 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/1c8500f7-10b2-406b-9385-d5b9480e2f71" /><br/> 113 + 114 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/77e312e2-af3c-4015-94f0-b1cf7409cc40" /><br/> 115 + 116 + <img width="700" alt="image" src="https://github.com/user-attachments/assets/c2aecd6e-d021-4149-b088-7cbf883a7e33" /> 117 + 118 + 1. (Optional) Let's keep it simple for our students and fellow teachers. Remove the sections about Desktop and Smartphone details. 119 + 120 + 1. Repeat the above steps for the "Feature request" template. 121 + 122 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/6456e261-fcd8-4845-b1ab-f2c2d5883c77" /> 123 + 124 + 1. With our templates prepared, let's commit them. In the top right, click the **Propose changes** button. Enter a description and set the branch to `add-issue-templates`, then click **Commit changes**. You can ignore the automatically created pull request. 125 + 126 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/a00a3740-ce0c-430c-9541-e56b7d9b45d6" /> 127 + 128 + 1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson. 129 + 130 + > [!TIP] 131 + > Did you notice that you are working in parallel on 2 branches now? That's exactly what working with multiple collaborators is like.
+130
.github/steps/4-prepare-for-the-inevitable.md
··· 1 + # Step 4: Prepare for the inevitable 2 + 3 + As you settle into the teachers' lounge with your coffee, you realize something: With more and more teachers contributing to the code, it's only a matter of time before security vulnerabilities creep in. 😱 4 + 5 + Every codebase, no matter how well-maintained, will eventually face security challenges. Let's try to proactively prepare for that day by configuring a few tools GitHub offers: 6 + 7 + 1. **Dependabot** - Track and create alerts for vulnerabilities found in upstream dependencies used in your project. Automatically create pull requests to upgrade dependencies to safe versions. 8 + 9 + 1. **Code Scanning** - Analyze your repository's code to find security vulnerabilities and coding errors. Use GitHub Copilot Autofix to automatically suggest fixes for these alerts. 10 + 11 + 1. **Security Policy and Private vulnerability reporting** - Provide a guide and simple form for security researchers and end users to responsibly report vulnerabilities directly to the repository maintainer. This prevents sensitive issues from being publicly disclosed before they're fixed. 12 + 13 + > [!NOTE] 14 + > This is just a quick setup guide. For a more detailed setup of each service, we recommend the related GitHub Skills exercises and/or GitHub documentation. 15 + 16 + ## ⌨️ Activity: Automate security updates with Dependabot 17 + 18 + Let's configure Dependabot to use default settings and automatically combine fixes for open alerts, and create pull requests. This will allow us to stay up to date with very little overhead! Nice! 19 + 20 + > [!TIP] 21 + > For a deeper dive, check out the [Secure Repository Supply Chain](https://github.com/skills/secure-repository-supply-chain) Skills exercise! 22 + 23 + 1. In the top navigation, select the **Settings** tab. 24 + 25 + 1. In the left navigation, select **Advanced Security**. 26 + 27 + 1. Find the **Dependabot** section. Verify or change the settings to match the following: 28 + 29 + - **Dependabot alerts**: `enabled` 30 + - **Dependabot security updates**: `enabled` 31 + - **Grouped security updates**: `enabled` 32 + - **Dependabot on Actions runners**: `enabled` 33 + 34 + 1. Find **Dependabot version updates** and click the **Enable** button. This will open an editor to create a configuration file. 35 + 36 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/a4d7ae19-0439-4b78-bcbf-9fce5c5410ff" /> 37 + 38 + 1. In the left files list, at the top, click the **Expand file tree** button to show the list of files. At the top, change the branch to `prepare-to-collaborate`. Remember, our ruleset won't let us directly change files on `main`. 39 + 40 + <img width="500" alt="image" src="https://github.com/user-attachments/assets/18a3cd1a-75ab-4e5e-a4c4-efd175d91ced" /> 41 + 42 + 1. Set the `package-ecosytem` to `pip` so Dependabot will automatically monitor our Python requirements. 43 + 44 + <img width="500" alt="image" src="https://github.com/user-attachments/assets/0bc90e67-4b71-4780-8272-20dc0fff5c4c" /> 45 + 46 + 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch. 47 + 48 + ## ⌨️ Activity: Detect dangerous patterns with code scanning 49 + 50 + None of us at the high school are professional software developers. Let's enable code scanning to alert us if we are potentially doing something unsafe. And, let's configure GitHub Copilot to create pull requests with solutions. 51 + 52 + > [!TIP] 53 + > Want to learn more about code scanning and writing custom queries? Check out the [Introduction to CodeQL](https://github.com/skills/introduction-to-codeql) Skills exercise after you finish this one! 54 + 55 + 1. In the top navigation, select the **Settings** tab. 56 + 57 + 1. In the left navigation, select **Advanced Security**. 58 + 59 + 1. Find the **Code scanning** section. Click the **Set up** button and select the **Default** option to open a configuration panel. 60 + 61 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/5b3a89e5-c71a-44d9-b917-d1a21dc52181" /> 62 + 63 + 1. Click the **Enable CodeQL** button to accept the default configuration. 64 + 65 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/6d5f7164-d8ed-4b5d-bbcf-8aed9e7acc5d" /> 66 + 67 + 1. Below the **Tools** section. Verify **Copilot Autofix** is set to `On`. 68 + 69 + <img width="350" alt="image" src="https://github.com/user-attachments/assets/b9d57e7a-f392-4c51-b137-f205a77adb79" /> 70 + 71 + ## ⌨️ Activity: Provide a safe path for security findings 72 + 73 + Now that the automated options are ready, let's create a guide for real-life humans to report any security vulnerabilities they find in a safe way. 74 + 75 + 1. In the top navigation, select the **Settings** tab. 76 + 77 + 1. In the left navigation, select **Advanced Security**. 78 + 79 + 1. Find the **Private vulnerability reporting** setting and verify it is `enabled`. 80 + 81 + 1. At the top navigation, click the **Security** tab. 82 + 83 + 1. In the left navigation, click the **Policy** option. 84 + 85 + 1. Click the **Start setup** button. An editor will be started to create the file `SECURITY.md`. 86 + 87 + <img width="350" alt="" src="https://github.com/user-attachments/assets/183b9fcc-1521-47fd-8165-b476a8ccb370"/><br/> 88 + 89 + <img width="350" alt="" src="https://github.com/user-attachments/assets/36c272d1-bc4a-48c8-b234-56173a214cdb"/> 90 + 91 + 1. In the left files list, at the top, click the **Expand file tree** button to show the list of files. At the top, change the branch to `prepare-to-collaborate`. Remember, our ruleset won't let us directly change files on `main`. 92 + 93 + 1. We will ignore the provided template and instead use a recommendation from Mergington High School's IT department. Add the following content: 94 + 95 + > 💡**Tip** If you switch to a branch that does not contain the same file, the editor will become empty. Press the **Restore** button to retrieve the previous editor's content. 96 + 97 + ```markdown 98 + # Mergington High School Security Policy 99 + 100 + ## Reporting a Vulnerability 101 + 102 + At Mergington High, we take the security of our Extra-Curricular Activities website seriously, especially 103 + since it contains student information. If you discover a security vulnerability, please follow these steps: 104 + 105 + 1. **Do not** create an issue on this repository, disclose the vulnerability publicly, or discuss it with other teachers/students. 106 + 1. In the top navigation of this repository, click the **Security** tab. 107 + 1. In the top right, click the **Report a vulnerability** button. 108 + 1. Fill out the provided form. It will request information like: 109 + - A description of the vulnerability 110 + - Steps to reproduce the issue 111 + - Potential impact on student data or website functionality 112 + - Suggested fix (if you have one) 113 + 1. Email the IT Club faculty advisor at techsupport@mergingtonhigh.example.edu and inform them you have made a report. **Do not** include any vulnerability details. 114 + 115 + ## Response Timeline 116 + 117 + - We will acknowledge receipt of your report within 2 school days 118 + - We will provide an initial assessment within 5 school days 119 + - Critical issues affecting student data will be addressed immediately 120 + - We will create a private fork to solve the issue and invite you as a collaborator so you can see our progress and contribute. 121 + 122 + ## Thank You 123 + 124 + Your help in keeping our school's digital resources secure is greatly appreciated! 125 + Responsible disclosure of security vulnerabilities helps protect our entire school community. 126 + ``` 127 + 128 + 1. In the top right, use the **Commit changes...** button and commit your changes directly to `prepare-to-collaborate` branch. 129 + 130 + 1. With the files committed, wait a moment for Mona to check your work, provide feedback, and share the next lesson.
+13
.github/steps/5-merge.md
··· 1 + # Step 5: Release 2 + 3 + With all our preparations ready, it's time to release them! 4 + 5 + ## ⌨️ Activity: Merge our collaboration changes 6 + 7 + 1. In the top navigation, select the **Pull requests** tab. 8 + 9 + 1. Find the pull request for the `prepare-to-collaborate` branch and merge it. You may need to wait for your new security scans to finish. 10 + 11 + 1. Find the pull request for the `add-issue-templates` branch and merge it. You may need to wait for your new security scans to finish. 12 + 13 + 1. With both pull requests merged, Mona will prepare the final review and acknowledge the exercise as finished! Nice work! You are all done! 🎉
+36
.github/steps/x-review.md
··· 1 + ## Review 2 + 3 + _Congratulations, you've completed this exercise! You're all set for an awesome time collaborating with your fellow teachers!_ 4 + 5 + <img src="https://octodex.github.com/images/jetpacktocat.png" alt=celebrate width=150 align=right> 6 + 7 + You've successfully prepared Mergington High's extracurricular activities website for healthy and safe collaboration. Make sure you let the principal know so he can brag to the IT department about your proactive efforts! 8 + 9 + Here is a snippet of something you can share: 10 + 11 + - Protected our code from accidental mistakes with `.gitignore` and branch protections. 12 + - Set clear guidelines for teacher contributions with `CONTRIBUTING.md` and `CODEOWNERS`. 13 + - Established community standards with a Code of Conduct and structured issue templates. 14 + - Prepared for the future security challenges by enabling automated scanning and providing safe submission procedures. 15 + 16 + ### What's next? 17 + 18 + This exercise was meant to introduce you to many of the different areas of managing a repository. However, there is still more to learn! 19 + 20 + Here are some additional exercises for a deeper dive: 21 + 22 + - [Skills: Secure your repository supply chain](https://github.com/skills/secure-repository-supply-chain) 23 + - [Skills: Introduction to CodeQL](https://github.com/skills/introduction-to-codeql) 24 + - [Skills: Introduction to Secret Scanning](https://github.com/skills/introduction-to-secret-scanning) 25 + 26 + Here are some useful references from the [GitHub Docs](https://docs.github.com/en): 27 + 28 + - [How to ignore files](https://docs.github.com/en/get-started/git-basics/ignoring-files) 29 + - [Template gitignore files](https://github.com/github/gitignore) 30 + - [Creating rulesets for a repository](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository#using-fnmatch-syntax) 31 + - [Managing a branch protection rule](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule) 32 + - [About code owners](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) 33 + - [Setting guidelines for contributors](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/setting-guidelines-for-repository-contributors) 34 + - [Add a code of conduct](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-a-code-of-conduct-to-your-project) 35 + - [Configuring default setup for code scanning](https://docs.github.com/en/code-security/code-scanning/enabling-code-scanning/configuring-default-setup-for-code-scanning) 36 + - [Adding a security policy](https://docs.github.com/en/code-security/getting-started/adding-a-security-policy-to-your-repository)
+79
.github/workflows/0-start-exercise.yml
··· 1 + name: Step 0 # Start Exercise 2 + 3 + on: 4 + push: 5 + branches: 6 + - main 7 + 8 + permissions: 9 + contents: write 10 + actions: write 11 + issues: write 12 + 13 + env: 14 + STEP_1_FILE: ".github/steps/1-protect-your-code.md" 15 + 16 + jobs: 17 + start_exercise: 18 + if: | 19 + !github.event.repository.is_template 20 + name: Start Exercise 21 + uses: skills/exercise-toolkit/.github/workflows/start-exercise.yml@v0.3.0 22 + with: 23 + exercise-title: "Introduction to Repository Management" 24 + intro-message: "In this exercise, you'll configure your repository for easier collaboration. You'll learn how to protect your code, prepare for collaboration, and foster healthy growth in your projects." 25 + 26 + post_next_step_content: 27 + name: Post next step content 28 + runs-on: ubuntu-latest 29 + needs: [start_exercise] 30 + env: 31 + ISSUE_URL: ${{ needs.start_exercise.outputs.issue-url }} 32 + 33 + steps: 34 + 35 + - name: Checkout 36 + uses: actions/checkout@v4 37 + 38 + - name: Load Exercise Toolkit 39 + uses: actions/checkout@v4 40 + with: 41 + repository: skills/exercise-toolkit 42 + path: exercise-toolkit 43 + ref: v0.3.0 44 + 45 + - name: Configure Git user 46 + run: | 47 + git config user.name github-actions[bot] 48 + git config user.email github-actions[bot]@users.noreply.github.com 49 + 50 + - name: Build comment - add step content 51 + id: build-comment 52 + uses: skills/action-text-variables@v2 53 + with: 54 + template-file: ${{ env.STEP_1_FILE }} 55 + template-vars: | 56 + login: ${{ github.actor }} 57 + full_repo_name: ${{ github.repository }} 58 + 59 + - name: Create comment - add step content 60 + run: | 61 + gh issue comment "$ISSUE_URL" \ 62 + --body "$ISSUE_BODY" 63 + env: 64 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 + ISSUE_BODY: ${{ steps.build-comment.outputs.updated-text }} 66 + 67 + - name: Create comment - watching for progress 68 + run: | 69 + gh issue comment "$ISSUE_URL" \ 70 + --body-file "exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md" 71 + env: 72 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 + 74 + - name: Disable current workflow and enable next one 75 + run: | 76 + # gh workflow enable "Step 0" # Already disabled 77 + gh workflow enable "Step 1" || true 78 + env: 79 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+112
.github/workflows/1-protect-your-code.yml
··· 1 + name: Step 1 2 + 3 + on: 4 + push: 5 + branches: 6 + - prepare-to-collaborate 7 + paths: 8 + - ".gitignore" 9 + 10 + permissions: 11 + contents: write 12 + actions: write 13 + issues: write 14 + 15 + env: 16 + STEP_2_FILE: ".github/steps/2-prepare-to-collaborate.md" 17 + 18 + jobs: 19 + find_exercise: 20 + if: | 21 + !github.event.repository.is_template 22 + name: Find Exercise Issue 23 + uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0 24 + 25 + check_step_work: 26 + name: Check step work 27 + runs-on: ubuntu-latest 28 + needs: [find_exercise] 29 + env: 30 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 31 + 32 + steps: 33 + - name: Checkout 34 + uses: actions/checkout@v4 35 + 36 + - name: Load Exercise Toolkit 37 + uses: actions/checkout@v4 38 + with: 39 + repository: skills/exercise-toolkit 40 + path: exercise-toolkit 41 + ref: v0.3.0 42 + 43 + # START: Check practical exercise 44 + 45 + # Creating the new .gitignore file is enough to finish this step. 46 + 47 + # END: Check practical exercise 48 + 49 + - name: Build message - step finished 50 + id: build-message-step-finish 51 + uses: skills/action-text-variables@v2 52 + with: 53 + template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md 54 + template-vars: | 55 + next_step_number: 2 56 + 57 + - name: Update comment - step finished 58 + run: | 59 + gh issue comment "$ISSUE_URL" \ 60 + --body "$ISSUE_BODY" \ 61 + --edit-last 62 + env: 63 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 + ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }} 65 + 66 + post_next_step_content: 67 + name: Post next step content 68 + runs-on: ubuntu-latest 69 + needs: [find_exercise, check_step_work] 70 + env: 71 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 72 + 73 + steps: 74 + - name: Checkout 75 + uses: actions/checkout@v4 76 + 77 + - name: Get Exercise Toolkit 78 + uses: actions/checkout@v4 79 + with: 80 + repository: skills/exercise-toolkit 81 + path: exercise-toolkit 82 + ref: v0.3.0 83 + 84 + - name: Build message - add step content 85 + id: build-message-add-step-content 86 + uses: skills/action-text-variables@v2 87 + with: 88 + template-file: ${{ env.STEP_2_FILE }} 89 + template-vars: | 90 + login: ${{ github.actor }} 91 + 92 + - name: Create comment - step results 93 + run: | 94 + gh issue comment "$ISSUE_URL" \ 95 + --body "$COMMENT_BODY" 96 + env: 97 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 + COMMENT_BODY: ${{ steps.build-message-add-step-content.outputs.updated-text }} 99 + 100 + - name: Create comment - watching for progress 101 + run: | 102 + gh issue comment "$ISSUE_URL" \ 103 + --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md 104 + env: 105 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 + 107 + - name: Disable current workflow and enable next one 108 + run: | 109 + gh workflow disable "Step 1" || true 110 + gh workflow enable "Step 2" || true 111 + env: 112 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+197
.github/workflows/2-prepare-to-collaborate.yml
··· 1 + name: Step 2 2 + 3 + on: 4 + push: 5 + branches: 6 + - prepare-to-collaborate 7 + paths: 8 + - "CONTRIBUTING.md" 9 + - "CODEOWNERS" 10 + 11 + permissions: 12 + contents: read 13 + actions: write 14 + issues: write 15 + 16 + env: 17 + STEP_3_FILE: ".github/steps/3-foster-healthy-growth.md" 18 + 19 + jobs: 20 + find_exercise: 21 + if: | 22 + !github.event.repository.is_template 23 + name: Find Exercise Issue 24 + uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0 25 + 26 + check_step_work: 27 + name: Check step work 28 + runs-on: ubuntu-latest 29 + needs: [find_exercise] 30 + env: 31 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 32 + 33 + steps: 34 + - name: Checkout 35 + uses: actions/checkout@v4 36 + 37 + - name: Get Exercise Toolkit 38 + uses: actions/checkout@v4 39 + with: 40 + repository: skills/exercise-toolkit 41 + path: exercise-toolkit 42 + ref: v0.3.0 43 + 44 + - name: Update comment - checking work 45 + run: | 46 + gh issue comment "$ISSUE_URL" \ 47 + --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \ 48 + --edit-last 49 + env: 50 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 + 52 + # START: Check practical exercise 53 + 54 + - name: Check CONTRIBUTING.md exists 55 + id: check-contributing 56 + uses: actions/github-script@v7 57 + with: 58 + script: | 59 + const fs = require('fs'); 60 + 61 + // Result object to store the message 62 + let result = { 63 + name: 'CONTRIBUTING.md', 64 + passed: true, 65 + message: '' 66 + } 67 + 68 + // Check that file exists 69 + if (!fs.existsSync('CONTRIBUTING.md')) { 70 + result.passed = false; 71 + result.message = 'File is missing.'; 72 + } 73 + 74 + return result; 75 + 76 + - name: Check CODEOWNERS exists 77 + id: check-codeowners 78 + uses: actions/github-script@v7 79 + with: 80 + script: | 81 + const fs = require('fs'); 82 + 83 + // Result object to store the message 84 + let result = { 85 + name: 'CODEOWNERS', 86 + passed: true, 87 + message: '' 88 + } 89 + 90 + // Check that file exists 91 + if (!fs.existsSync('CODEOWNERS')) { 92 + result.passed = false; 93 + result.message = 'File is missing.'; 94 + } 95 + 96 + return result; 97 + 98 + - name: Check all results 99 + id: check-all-results 100 + uses: actions/github-script@v7 101 + with: 102 + script: | 103 + const checks = [ 104 + JSON.parse(process.env['check1']), 105 + JSON.parse(process.env['check2']), 106 + ]; 107 + 108 + const result = checks.every(check => check.passed); 109 + return result 110 + env: 111 + check1: ${{ steps.check-contributing.outputs.result }} 112 + check2: ${{ steps.check-codeowners.outputs.result }} 113 + 114 + - name: Build message - step results 115 + id: build-message-step-results 116 + uses: skills/action-text-variables@v2 117 + with: 118 + template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md 119 + template-vars: > 120 + { 121 + "step_number": 3, 122 + "passed": ${{ steps.check-all-results.outputs.result }}, 123 + "results_table": [ 124 + ${{ steps.check-contributing.outputs.result }}, 125 + ${{ steps.check-codeowners.outputs.result }} 126 + ] 127 + } 128 + 129 + - name: Create comment - step results 130 + run: | 131 + gh issue comment "$ISSUE_URL" \ 132 + --body "$COMMENT_BODY" \ 133 + --edit-last 134 + env: 135 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 136 + COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }} 137 + 138 + - name: Fail job if not all checks passed 139 + if: steps.check-all-results.outputs.result == 'false' 140 + run: exit 1 141 + 142 + # END: Check practical exercise 143 + 144 + - name: Build message - step finished 145 + id: build-message-step-finish 146 + uses: skills/action-text-variables@v2 147 + with: 148 + template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md 149 + template-vars: | 150 + next_step_number: 3 151 + 152 + - name: Update comment - step finished 153 + run: | 154 + gh issue comment "$ISSUE_URL" \ 155 + --body "$ISSUE_BODY" 156 + env: 157 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 158 + ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }} 159 + 160 + post_next_step_content: 161 + name: Post next step content 162 + runs-on: ubuntu-latest 163 + needs: [find_exercise, check_step_work] 164 + env: 165 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 166 + 167 + steps: 168 + - name: Checkout 169 + uses: actions/checkout@v4 170 + 171 + - name: Get Exercise Toolkit 172 + uses: actions/checkout@v4 173 + with: 174 + repository: skills/exercise-toolkit 175 + path: exercise-toolkit 176 + ref: v0.3.0 177 + 178 + - name: Create comment - add step content 179 + run: | 180 + gh issue comment "$ISSUE_URL" \ 181 + --body-file "$STEP_3_FILE" 182 + env: 183 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 184 + 185 + - name: Create comment - watching for progress 186 + run: | 187 + gh issue comment "$ISSUE_URL" \ 188 + --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md 189 + env: 190 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 191 + 192 + - name: Disable current workflow and enable next one 193 + run: | 194 + gh workflow disable "Step 2" || true 195 + gh workflow enable "Step 3" || true 196 + env: 197 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+233
.github/workflows/3-foster-healthy-growth.yml
··· 1 + name: Step 3 2 + 3 + on: 4 + push: 5 + branches: 6 + - add-issue-templates 7 + - prepare-to-collaborate 8 + paths: 9 + - ".github/ISSUE_TEMPLATE/**" 10 + - "CODE_OF_CONDUCT.md" 11 + 12 + permissions: 13 + contents: read 14 + actions: write 15 + issues: write 16 + 17 + env: 18 + STEP_4_FILE: ".github/steps/4-prepare-for-the-inevitable.md" 19 + 20 + jobs: 21 + find_exercise: 22 + if: | 23 + !github.event.repository.is_template 24 + name: Find Exercise Issue 25 + uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0 26 + 27 + check_step_work: 28 + name: Check step work 29 + runs-on: ubuntu-latest 30 + needs: [find_exercise] 31 + env: 32 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 33 + 34 + steps: 35 + - name: Checkout branch - prepare-to-collaborate 36 + uses: actions/checkout@v4 37 + with: 38 + path: prepare-to-collaborate 39 + ref: prepare-to-collaborate 40 + 41 + - name: Checkout branch - add-issue-templates 42 + uses: actions/checkout@v4 43 + with: 44 + path: add-issue-templates 45 + ref: add-issue-templates 46 + 47 + - name: Get Exercise Toolkit 48 + uses: actions/checkout@v4 49 + with: 50 + repository: skills/exercise-toolkit 51 + path: exercise-toolkit 52 + ref: v0.3.0 53 + 54 + - name: Update comment - checking work 55 + run: | 56 + gh issue comment "$ISSUE_URL" \ 57 + --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \ 58 + --edit-last 59 + env: 60 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 + 62 + # START: Check practical exercise 63 + 64 + - name: Check for Code of Conduct 65 + id: check-code-of-conduct 66 + uses: actions/github-script@v7 67 + with: 68 + script: | 69 + const fs = require('fs'); 70 + 71 + // Result object to store the message 72 + let result = { 73 + name: 'CODE_OF_CONDUCT.md', 74 + passed: true, 75 + message: '' 76 + } 77 + 78 + // Check that file exists 79 + if (!fs.existsSync('prepare-to-collaborate/CODE_OF_CONDUCT.md')) { 80 + result.passed = false; 81 + result.message = 'File is missing.'; 82 + } 83 + 84 + return result; 85 + 86 + - name: Check for Bug Report Templates 87 + id: check-bug-report-template 88 + uses: actions/github-script@v7 89 + with: 90 + script: | 91 + const fs = require('fs'); 92 + 93 + // Result object to store the message 94 + let result = { 95 + name: 'bug_report.md', 96 + passed: true, 97 + message: '' 98 + } 99 + 100 + // Check that file exists 101 + if (!fs.existsSync('add-issue-templates/.github/ISSUE_TEMPLATE/bug_report.md')) { 102 + result.passed = false; 103 + result.message = 'File is missing.'; 104 + } 105 + 106 + return result; 107 + 108 + - name: Check for Feature Request Templates 109 + id: check-feature-request-template 110 + uses: actions/github-script@v7 111 + continue-on-error: true 112 + with: 113 + script: | 114 + const fs = require('fs'); 115 + 116 + // Result object to store the message 117 + let result = { 118 + name: 'feature_request.md', 119 + passed: true, 120 + message: '' 121 + } 122 + 123 + // Check that file exists 124 + if (!fs.existsSync('add-issue-templates/.github/ISSUE_TEMPLATE/feature_request.md')) { 125 + result.passed = false; 126 + result.message = 'File is missing.'; 127 + } 128 + 129 + return result; 130 + 131 + - name: Check all results 132 + id: check-all-results 133 + uses: actions/github-script@v7 134 + with: 135 + script: | 136 + const checks = [ 137 + JSON.parse(process.env['check1']), 138 + JSON.parse(process.env['check2']), 139 + JSON.parse(process.env['check3']) 140 + ]; 141 + 142 + const result = checks.every(check => check.passed); 143 + return result 144 + env: 145 + check1: ${{ steps.check-code-of-conduct.outputs.result }} 146 + check2: ${{ steps.check-bug-report-template.outputs.result }} 147 + check3: ${{ steps.check-feature-request-template.outputs.result }} 148 + 149 + - name: Build message - step results 150 + id: build-message-step-results 151 + uses: skills/action-text-variables@v2 152 + with: 153 + template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md 154 + template-vars: > 155 + { 156 + "step_number": 4, 157 + "passed": ${{ steps.check-all-results.outputs.result }}, 158 + "results_table": [ 159 + ${{ steps.check-code-of-conduct.outputs.result }}, 160 + ${{ steps.check-bug-report-template.outputs.result }}, 161 + ${{ steps.check-feature-request-template.outputs.result }} 162 + ] 163 + } 164 + 165 + - name: Create comment - step results 166 + run: | 167 + gh issue comment "$ISSUE_URL" \ 168 + --body "$COMMENT_BODY" \ 169 + --edit-last 170 + env: 171 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 172 + COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }} 173 + 174 + - name: Fail job if not all checks passed 175 + if: steps.check-all-results.outputs.result == 'false' 176 + run: exit 1 177 + 178 + # END: Check practical exercise 179 + 180 + - name: Build message - step finished 181 + id: build-message-step-finish 182 + uses: skills/action-text-variables@v2 183 + with: 184 + template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md 185 + template-vars: | 186 + next_step_number: 4 187 + 188 + - name: Update comment - step finished 189 + run: | 190 + gh issue comment "$ISSUE_URL" \ 191 + --body "$ISSUE_BODY" 192 + env: 193 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 194 + ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }} 195 + 196 + post_next_step_content: 197 + name: Post next step content 198 + needs: [find_exercise, check_step_work] 199 + runs-on: ubuntu-latest 200 + env: 201 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 202 + 203 + steps: 204 + - name: Checkout 205 + uses: actions/checkout@v4 206 + 207 + - name: Get Exercise Toolkit 208 + uses: actions/checkout@v4 209 + with: 210 + repository: skills/exercise-toolkit 211 + path: exercise-toolkit 212 + ref: v0.3.0 213 + 214 + - name: Create comment - add step content 215 + run: | 216 + gh issue comment "$ISSUE_URL" \ 217 + --body-file "$STEP_4_FILE" 218 + env: 219 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 220 + 221 + - name: Create comment - watching for progress 222 + run: | 223 + gh issue comment "$ISSUE_URL" \ 224 + --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md 225 + env: 226 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 227 + 228 + - name: Disable current workflow and enable next one 229 + run: | 230 + gh workflow disable "Step 3" || true 231 + gh workflow enable "Step 4" || true 232 + env: 233 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+197
.github/workflows/4-prepare-for-the-inevitable.yml
··· 1 + name: Step 4 2 + 3 + on: 4 + push: 5 + branches: 6 + - prepare-to-collaborate 7 + paths: 8 + - ".github/dependabot.yml" 9 + - "SECURITY.md" 10 + 11 + permissions: 12 + contents: write 13 + actions: write 14 + issues: write 15 + 16 + env: 17 + STEP_5_FILE: ".github/steps/5-merge.md" 18 + 19 + jobs: 20 + find_exercise: 21 + if: | 22 + !github.event.repository.is_template 23 + name: Find Exercise Issue 24 + uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0 25 + 26 + check_step_work: 27 + name: Check step work 28 + runs-on: ubuntu-latest 29 + needs: [find_exercise] 30 + env: 31 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 32 + 33 + steps: 34 + - name: Checkout 35 + uses: actions/checkout@v4 36 + 37 + - name: Get Exercise Toolkit 38 + uses: actions/checkout@v4 39 + with: 40 + repository: skills/exercise-toolkit 41 + path: exercise-toolkit 42 + ref: v0.3.0 43 + 44 + - name: Update comment - checking work 45 + run: | 46 + gh issue comment "$ISSUE_URL" \ 47 + --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \ 48 + --edit-last 49 + env: 50 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 + 52 + # START: Check practical exercise 53 + 54 + - name: Check for Dependabot config 55 + id: check-dependabot-config 56 + uses: actions/github-script@v7 57 + with: 58 + script: | 59 + const fs = require('fs'); 60 + 61 + // Result object to store the message 62 + let result = { 63 + name: 'dependabot.yml', 64 + passed: true, 65 + message: '' 66 + } 67 + 68 + // Check that file exists 69 + if (!fs.existsSync('.github/dependabot.yml')) { 70 + result.passed = false; 71 + result.message = 'File is missing.'; 72 + } 73 + 74 + return result; 75 + 76 + - name: Check for Security Policy 77 + id: check-security-policy 78 + uses: actions/github-script@v7 79 + with: 80 + script: | 81 + const fs = require('fs'); 82 + 83 + // Result object to store the message 84 + let result = { 85 + name: 'SECURITY.md', 86 + passed: true, 87 + message: '' 88 + } 89 + 90 + // Check that file exists 91 + if (!fs.existsSync('SECURITY.md')) { 92 + result.passed = false; 93 + result.message = 'File is missing.'; 94 + } 95 + 96 + return result; 97 + 98 + - name: Check all results 99 + id: check-all-results 100 + uses: actions/github-script@v7 101 + with: 102 + script: | 103 + const checks = [ 104 + JSON.parse(process.env['check1']), 105 + JSON.parse(process.env['check2']) 106 + ]; 107 + 108 + const result = checks.every(check => check.passed); 109 + return result 110 + env: 111 + check1: ${{ steps.check-dependabot-config.outputs.result }} 112 + check2: ${{ steps.check-security-policy.outputs.result }} 113 + 114 + - name: Build message - step results 115 + id: build-message-step-results 116 + uses: skills/action-text-variables@v2 117 + with: 118 + template-file: exercise-toolkit/markdown-templates/step-feedback/step-results.md 119 + template-vars: > 120 + { 121 + "step_number": 4, 122 + "passed": ${{ steps.check-all-results.outputs.result }}, 123 + "results_table": [ 124 + ${{ steps.check-dependabot-config.outputs.result }}, 125 + ${{ steps.check-security-policy.outputs.result }} 126 + ] 127 + } 128 + 129 + - name: Create comment - step results 130 + run: | 131 + gh issue comment "$ISSUE_URL" \ 132 + --body "$COMMENT_BODY" \ 133 + --edit-last 134 + env: 135 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 136 + COMMENT_BODY: ${{ steps.build-message-step-results.outputs.updated-text }} 137 + 138 + - name: Fail job if not all checks passed 139 + if: steps.check-all-results.outputs.result == 'false' 140 + run: exit 1 141 + 142 + # END: Check practical exercise 143 + 144 + - name: Build message - step finished 145 + id: build-message-step-finish 146 + uses: skills/action-text-variables@v2 147 + with: 148 + template-file: exercise-toolkit/markdown-templates/step-feedback/step-finished-prepare-next-step.md 149 + template-vars: | 150 + next_step_number: 5 151 + 152 + - name: Update comment - step finished 153 + run: | 154 + gh issue comment "$ISSUE_URL" \ 155 + --body "$ISSUE_BODY" 156 + env: 157 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 158 + ISSUE_BODY: ${{ steps.build-message-step-finish.outputs.updated-text }} 159 + 160 + post_next_step_content: 161 + name: Post next step content 162 + needs: [find_exercise, check_step_work] 163 + runs-on: ubuntu-latest 164 + env: 165 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 166 + 167 + steps: 168 + - name: Checkout 169 + uses: actions/checkout@v4 170 + 171 + - name: Get Exercise Toolkit 172 + uses: actions/checkout@v4 173 + with: 174 + repository: skills/exercise-toolkit 175 + path: exercise-toolkit 176 + ref: v0.3.0 177 + 178 + - name: Create comment - add step content 179 + run: | 180 + gh issue comment "$ISSUE_URL" \ 181 + --body-file "$STEP_5_FILE" 182 + env: 183 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 184 + 185 + - name: Create comment - watching for progress 186 + run: | 187 + gh issue comment "$ISSUE_URL" \ 188 + --body-file exercise-toolkit/markdown-templates/step-feedback/watching-for-progress.md 189 + env: 190 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 191 + 192 + - name: Disable current workflow and enable next one 193 + run: | 194 + gh workflow disable "Step 4" || true 195 + gh workflow enable "Step 5" || true 196 + env: 197 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+107
.github/workflows/5-merge.yml
··· 1 + name: Step 5 2 + 3 + on: 4 + pull_request: 5 + branches: 6 + - main 7 + types: 8 + - closed 9 + 10 + concurrency: 11 + group: ${{ github.workflow }} 12 + cancel-in-progress: true 13 + 14 + permissions: 15 + contents: write 16 + actions: write 17 + issues: write 18 + 19 + env: 20 + REVIEW_FILE: ".github/steps/x-review.md" 21 + 22 + jobs: 23 + find_exercise: 24 + if: | 25 + !github.event.repository.is_template 26 + name: Find Exercise Issue 27 + uses: skills/exercise-toolkit/.github/workflows/find-exercise-issue.yml@v0.3.0 28 + 29 + check_step_work: 30 + name: Check step work 31 + if: github.event.pull_request.merged == true 32 + runs-on: ubuntu-latest 33 + needs: [find_exercise] 34 + env: 35 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 36 + 37 + steps: 38 + - name: Checkout 39 + uses: actions/checkout@v4 40 + 41 + - name: Get Exercise Toolkit 42 + uses: actions/checkout@v4 43 + with: 44 + repository: skills/exercise-toolkit 45 + path: exercise-toolkit 46 + ref: v0.3.0 47 + 48 + - name: Update comment - checking work 49 + run: | 50 + gh issue comment "$ISSUE_URL" \ 51 + --body-file exercise-toolkit/markdown-templates/step-feedback/checking-work.md \ 52 + --edit-last 53 + env: 54 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 + 56 + # START: Check practical exercise 57 + 58 + # Merging the pull request is enough to finish this step 59 + 60 + # END: Check practical exercise 61 + 62 + - name: Update comment - step finished - final review next 63 + run: | 64 + gh issue comment "$ISSUE_URL" \ 65 + --body-file exercise-toolkit/markdown-templates/step-feedback/lesson-review.md \ 66 + --edit-last 67 + env: 68 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 + 70 + post_review_content: 71 + name: Post review content 72 + runs-on: ubuntu-latest 73 + needs: [find_exercise, check_step_work] 74 + env: 75 + ISSUE_URL: ${{ needs.find_exercise.outputs.issue-url }} 76 + 77 + steps: 78 + - name: Checkout 79 + uses: actions/checkout@v4 80 + 81 + - name: Create comment - add review content 82 + run: | 83 + gh issue comment "$ISSUE_URL" \ 84 + --body-file ${{ env.REVIEW_FILE }} 85 + env: 86 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 87 + 88 + finish_exercise: 89 + name: Finish Exercise 90 + needs: [find_exercise, post_review_content] 91 + uses: skills/exercise-toolkit/.github/workflows/finish-exercise.yml@57ecd963a3e54e6b01a30ebab57d5202a7afca3f 92 + with: 93 + issue-url: ${{ needs.find_exercise.outputs.issue-url }} 94 + update-readme-with-congratulations: false 95 + 96 + disable_workflow: 97 + name: Disable this workflow 98 + needs: [find_exercise, post_review_content] 99 + runs-on: ubuntu-latest 100 + 101 + steps: 102 + - name: Checkout 103 + uses: actions/checkout@v4 104 + - name: Disable current workflow 105 + run: gh workflow disable "${{github.workflow}}" || true 106 + env: 107 + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+19
.vscode/launch.json
··· 1 + { 2 + // Use IntelliSense to learn about possible attributes. 3 + // Hover to view descriptions of existing attributes. 4 + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 + "version": "0.2.0", 6 + "configurations": [ 7 + { 8 + "name": "Launch Mergington WebApp", 9 + "type": "debugpy", 10 + "request": "launch", 11 + "module": "uvicorn", 12 + "args": [ 13 + "src.app:app", 14 + "--reload" 15 + ], 16 + "jinja": true 17 + } 18 + ] 19 + }
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 GitHub Skills 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+54
README.md
··· 1 + # Introduction to Repository Management 2 + 3 + _Learn the basics of several GitHub features that can help support a collaborative, friendly, and healthy project._ 4 + 5 + ## Welcome 6 + 7 + - **Who is this for**: Developers with the need to start collaborating. 8 + - **What you'll learn**: The different ways to protect your repository's content as more people join as collaborators. 9 + - **What you'll build**: You will prepare Mergington High School's extracurricular activities website repository so additional teachers can safely collaborate. 10 + - **Prerequisites**: 11 + - Skills exercise: [Introduction to GitHub](https://github.com/skills/introduction-to-github) 12 + - Skills exercise: [Communicate using Markdown](https://github.com/skills/communicate-using-markdown) 13 + - Skills exercise: [Review pull requests](https://github.com/skills/review-pull-requests) 14 + - **How long**: This exercise takes less than one hour to complete. 15 + 16 + In this exercise, you will: 17 + 18 + 1. Add a simple rulesets and configuration to restrict repository content. 19 + 1. Communicate procedures to help guide collaborators. 20 + 1. Assign responsibility of parts of the code to particular collaborators. 21 + 1. Learn the difference between collaboration in a personal repository and organization repository. 22 + 1. Establish ground rules to promote a health collaboration environment. 23 + 1. Establish a process for managing security updates. 24 + 25 + > [!IMPORTANT] 26 + > This exercise is meant to provide an overview of many GitHub features. 27 + > It will provide references to learn more but not a detailed explanation for any specific subject. 28 + 29 + ### How to start this exercise 30 + 31 + Simply copy the exercise to your account, then give your favorite Octocat (Mona) **about 20 seconds** to prepare the first lesson, then **refresh the page**. 32 + 33 + [![](https://img.shields.io/badge/Copy%20Exercise-%E2%86%92-1f883d?style=for-the-badge&logo=github&labelColor=197935)](https://github.com/new?template_owner=skills&template_name=introduction-to-repository-management&owner=%40me&name=skills-introduction-to-repository-management&description=Exercise:+introduction+to+repository+management&visibility=public) 34 + 35 + <details> 36 + <summary>Having trouble? 🤷</summary><br/> 37 + 38 + When copying the exercise, we recommend the following settings: 39 + 40 + - For owner, choose your personal account or an organization to host the repository. 41 + 42 + - We recommend creating a public repository, since private repositories will use Actions minutes. 43 + 44 + If the exercise isn't ready in 20 seconds, please check the [Actions](../../actions) tab. 45 + 46 + - Check to see if a job is running. Sometimes it simply takes a bit longer. 47 + 48 + - If the page shows a failed job, please submit an issue. Nice, you found a bug! 🐛 49 + 50 + </details> 51 + 52 + --- 53 + 54 + &copy; 2025 GitHub &bull; [Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/code_of_conduct.md) &bull; [MIT License](https://gh.io/mit)
+4
requirements.txt
··· 1 + fastapi 2 + uvicorn 3 + pymongo 4 + argon2-cffi==23.1.0
+50
src/README.md
··· 1 + # Mergington High School Activities API 2 + 3 + A super simple FastAPI application that allows students to view and sign up for extracurricular activities. 4 + 5 + ## Features 6 + 7 + - View all available extracurricular activities 8 + - Sign up for activities 9 + 10 + ## Getting Started 11 + 12 + 1. Install the dependencies: 13 + 14 + ``` 15 + pip install fastapi uvicorn 16 + ``` 17 + 18 + 2. Run the application: 19 + 20 + ``` 21 + python app.py 22 + ``` 23 + 24 + 3. Open your browser and go to: 25 + - API documentation: http://localhost:8000/docs 26 + - Alternative documentation: http://localhost:8000/redoc 27 + 28 + ## API Endpoints 29 + 30 + | Method | Endpoint | Description | 31 + | ------ | ----------------------------------------------------------------- | ------------------------------------------------------------------- | 32 + | GET | `/activities` | Get all activities with their details and current participant count | 33 + | POST | `/activities/{activity_name}/signup?email=student@mergington.edu` | Sign up for an activity | 34 + 35 + ## Data Model 36 + 37 + The application uses a simple data model with meaningful identifiers: 38 + 39 + 1. **Activities** - Uses activity name as identifier: 40 + 41 + - Description 42 + - Schedule 43 + - Maximum number of participants allowed 44 + - List of student emails who are signed up 45 + 46 + 2. **Students** - Uses email as identifier: 47 + - Name 48 + - Grade level 49 + 50 + All data is stored in memory, which means data will be reset when the server restarts.
+35
src/app.py
··· 1 + """ 2 + High School Management System API 3 + 4 + A super simple FastAPI application that allows students to view and sign up 5 + for extracurricular activities at Mergington High School. 6 + """ 7 + 8 + from fastapi import FastAPI 9 + from fastapi.staticfiles import StaticFiles 10 + from fastapi.responses import RedirectResponse 11 + import os 12 + from pathlib import Path 13 + from .backend import routers, database 14 + 15 + # Initialize web host 16 + app = FastAPI( 17 + title="Mergington High School API", 18 + description="API for viewing and signing up for extracurricular activities" 19 + ) 20 + 21 + # Initialize database with sample data if empty 22 + database.init_database() 23 + 24 + # Mount the static files directory for serving the frontend 25 + current_dir = Path(__file__).parent 26 + app.mount("/static", StaticFiles(directory=os.path.join(current_dir, "static")), name="static") 27 + 28 + # Root endpoint to redirect to static index.html 29 + @app.get("/") 30 + def root(): 31 + return RedirectResponse(url="/static/index.html") 32 + 33 + # Include routers 34 + app.include_router(routers.activities.router) 35 + app.include_router(routers.auth.router)
+2
src/backend/__init__.py
··· 1 + from . import routers 2 + from . import database
+189
src/backend/database.py
··· 1 + """ 2 + MongoDB database configuration and setup for Mergington High School API 3 + """ 4 + 5 + from pymongo import MongoClient 6 + from argon2 import PasswordHasher 7 + 8 + # Connect to MongoDB 9 + client = MongoClient('mongodb://localhost:27017/') 10 + db = client['mergington_high'] 11 + activities_collection = db['activities'] 12 + teachers_collection = db['teachers'] 13 + 14 + # Methods 15 + def hash_password(password): 16 + """Hash password using Argon2""" 17 + ph = PasswordHasher() 18 + return ph.hash(password) 19 + 20 + def init_database(): 21 + """Initialize database if empty""" 22 + 23 + # Initialize activities if empty 24 + if activities_collection.count_documents({}) == 0: 25 + for name, details in initial_activities.items(): 26 + activities_collection.insert_one({"_id": name, **details}) 27 + 28 + # Initialize teacher accounts if empty 29 + if teachers_collection.count_documents({}) == 0: 30 + for teacher in initial_teachers: 31 + teachers_collection.insert_one({"_id": teacher["username"], **teacher}) 32 + 33 + # Initial database if empty 34 + initial_activities = { 35 + "Chess Club": { 36 + "description": "Learn strategies and compete in chess tournaments", 37 + "schedule": "Mondays and Fridays, 3:15 PM - 4:45 PM", 38 + "schedule_details": { 39 + "days": ["Monday", "Friday"], 40 + "start_time": "15:15", 41 + "end_time": "16:45" 42 + }, 43 + "max_participants": 12, 44 + "participants": ["michael@mergington.edu", "daniel@mergington.edu"] 45 + }, 46 + "Programming Class": { 47 + "description": "Learn programming fundamentals and build software projects", 48 + "schedule": "Tuesdays and Thursdays, 7:00 AM - 8:00 AM", 49 + "schedule_details": { 50 + "days": ["Tuesday", "Thursday"], 51 + "start_time": "07:00", 52 + "end_time": "08:00" 53 + }, 54 + "max_participants": 20, 55 + "participants": ["emma@mergington.edu", "sophia@mergington.edu"] 56 + }, 57 + "Morning Fitness": { 58 + "description": "Early morning physical training and exercises", 59 + "schedule": "Mondays, Wednesdays, Fridays, 6:30 AM - 7:45 AM", 60 + "schedule_details": { 61 + "days": ["Monday", "Wednesday", "Friday"], 62 + "start_time": "06:30", 63 + "end_time": "07:45" 64 + }, 65 + "max_participants": 30, 66 + "participants": ["john@mergington.edu", "olivia@mergington.edu"] 67 + }, 68 + "Soccer Team": { 69 + "description": "Join the school soccer team and compete in matches", 70 + "schedule": "Tuesdays and Thursdays, 3:30 PM - 5:30 PM", 71 + "schedule_details": { 72 + "days": ["Tuesday", "Thursday"], 73 + "start_time": "15:30", 74 + "end_time": "17:30" 75 + }, 76 + "max_participants": 22, 77 + "participants": ["liam@mergington.edu", "noah@mergington.edu"] 78 + }, 79 + "Basketball Team": { 80 + "description": "Practice and compete in basketball tournaments", 81 + "schedule": "Wednesdays and Fridays, 3:15 PM - 5:00 PM", 82 + "schedule_details": { 83 + "days": ["Wednesday", "Friday"], 84 + "start_time": "15:15", 85 + "end_time": "17:00" 86 + }, 87 + "max_participants": 15, 88 + "participants": ["ava@mergington.edu", "mia@mergington.edu"] 89 + }, 90 + "Art Club": { 91 + "description": "Explore various art techniques and create masterpieces", 92 + "schedule": "Thursdays, 3:15 PM - 5:00 PM", 93 + "schedule_details": { 94 + "days": ["Thursday"], 95 + "start_time": "15:15", 96 + "end_time": "17:00" 97 + }, 98 + "max_participants": 15, 99 + "participants": ["amelia@mergington.edu", "harper@mergington.edu"] 100 + }, 101 + "Drama Club": { 102 + "description": "Act, direct, and produce plays and performances", 103 + "schedule": "Mondays and Wednesdays, 3:30 PM - 5:30 PM", 104 + "schedule_details": { 105 + "days": ["Monday", "Wednesday"], 106 + "start_time": "15:30", 107 + "end_time": "17:30" 108 + }, 109 + "max_participants": 20, 110 + "participants": ["ella@mergington.edu", "scarlett@mergington.edu"] 111 + }, 112 + "Math Club": { 113 + "description": "Solve challenging problems and prepare for math competitions", 114 + "schedule": "Tuesdays, 7:15 AM - 8:00 AM", 115 + "schedule_details": { 116 + "days": ["Tuesday"], 117 + "start_time": "07:15", 118 + "end_time": "08:00" 119 + }, 120 + "max_participants": 10, 121 + "participants": ["james@mergington.edu", "benjamin@mergington.edu"] 122 + }, 123 + "Debate Team": { 124 + "description": "Develop public speaking and argumentation skills", 125 + "schedule": "Fridays, 3:30 PM - 5:30 PM", 126 + "schedule_details": { 127 + "days": ["Friday"], 128 + "start_time": "15:30", 129 + "end_time": "17:30" 130 + }, 131 + "max_participants": 12, 132 + "participants": ["charlotte@mergington.edu", "amelia@mergington.edu"] 133 + }, 134 + "Weekend Robotics Workshop": { 135 + "description": "Build and program robots in our state-of-the-art workshop", 136 + "schedule": "Saturdays, 10:00 AM - 2:00 PM", 137 + "schedule_details": { 138 + "days": ["Saturday"], 139 + "start_time": "10:00", 140 + "end_time": "14:00" 141 + }, 142 + "max_participants": 15, 143 + "participants": ["ethan@mergington.edu", "oliver@mergington.edu"] 144 + }, 145 + "Science Olympiad": { 146 + "description": "Weekend science competition preparation for regional and state events", 147 + "schedule": "Saturdays, 1:00 PM - 4:00 PM", 148 + "schedule_details": { 149 + "days": ["Saturday"], 150 + "start_time": "13:00", 151 + "end_time": "16:00" 152 + }, 153 + "max_participants": 18, 154 + "participants": ["isabella@mergington.edu", "lucas@mergington.edu"] 155 + }, 156 + "Sunday Chess Tournament": { 157 + "description": "Weekly tournament for serious chess players with rankings", 158 + "schedule": "Sundays, 2:00 PM - 5:00 PM", 159 + "schedule_details": { 160 + "days": ["Sunday"], 161 + "start_time": "14:00", 162 + "end_time": "17:00" 163 + }, 164 + "max_participants": 16, 165 + "participants": ["william@mergington.edu", "jacob@mergington.edu"] 166 + } 167 + } 168 + 169 + initial_teachers = [ 170 + { 171 + "username": "mrodriguez", 172 + "display_name": "Ms. Rodriguez", 173 + "password": hash_password("art123"), 174 + "role": "teacher" 175 + }, 176 + { 177 + "username": "mchen", 178 + "display_name": "Mr. Chen", 179 + "password": hash_password("chess456"), 180 + "role": "teacher" 181 + }, 182 + { 183 + "username": "principal", 184 + "display_name": "Principal Martinez", 185 + "password": hash_password("admin789"), 186 + "role": "admin" 187 + } 188 + ] 189 +
+2
src/backend/routers/__init__.py
··· 1 + from . import activities 2 + from . import auth
+127
src/backend/routers/activities.py
··· 1 + """ 2 + Endpoints for the High School Management System API 3 + """ 4 + 5 + from fastapi import APIRouter, HTTPException, Query 6 + from fastapi.responses import RedirectResponse 7 + from typing import Dict, Any, Optional, List 8 + 9 + from ..database import activities_collection, teachers_collection 10 + 11 + router = APIRouter( 12 + prefix="/activities", 13 + tags=["activities"] 14 + ) 15 + 16 + @router.get("/", response_model=Dict[str, Any]) 17 + def get_activities( 18 + day: Optional[str] = None, 19 + start_time: Optional[str] = None, 20 + end_time: Optional[str] = None 21 + ) -> Dict[str, Any]: 22 + """ 23 + Get all activities with their details, with optional filtering by day and time 24 + 25 + - day: Filter activities occurring on this day (e.g., 'Monday', 'Tuesday') 26 + - start_time: Filter activities starting at or after this time (24-hour format, e.g., '14:30') 27 + - end_time: Filter activities ending at or before this time (24-hour format, e.g., '17:00') 28 + """ 29 + # Build the query based on provided filters 30 + query = {} 31 + 32 + if day: 33 + query["schedule_details.days"] = {"$in": [day]} 34 + 35 + if start_time: 36 + query["schedule_details.start_time"] = {"$gte": start_time} 37 + 38 + if end_time: 39 + query["schedule_details.end_time"] = {"$lte": end_time} 40 + 41 + # Query the database 42 + activities = {} 43 + for activity in activities_collection.find(query): 44 + name = activity.pop('_id') 45 + activities[name] = activity 46 + 47 + return activities 48 + 49 + @router.get("/days", response_model=List[str]) 50 + def get_available_days() -> List[str]: 51 + """Get a list of all days that have activities scheduled""" 52 + # Aggregate to get unique days across all activities 53 + pipeline = [ 54 + {"$unwind": "$schedule_details.days"}, 55 + {"$group": {"_id": "$schedule_details.days"}}, 56 + {"$sort": {"_id": 1}} # Sort days alphabetically 57 + ] 58 + 59 + days = [] 60 + for day_doc in activities_collection.aggregate(pipeline): 61 + days.append(day_doc["_id"]) 62 + 63 + return days 64 + 65 + @router.post("/{activity_name}/signup") 66 + def signup_for_activity(activity_name: str, email: str, teacher_username: Optional[str] = Query(None)): 67 + """Sign up a student for an activity - requires teacher authentication""" 68 + # Check teacher authentication 69 + if not teacher_username: 70 + raise HTTPException(status_code=401, detail="Authentication required for this action") 71 + 72 + teacher = teachers_collection.find_one({"_id": teacher_username}) 73 + if not teacher: 74 + raise HTTPException(status_code=401, detail="Invalid teacher credentials") 75 + 76 + # Get the activity 77 + activity = activities_collection.find_one({"_id": activity_name}) 78 + if not activity: 79 + raise HTTPException(status_code=404, detail="Activity not found") 80 + 81 + # Validate student is not already signed up 82 + if email in activity["participants"]: 83 + raise HTTPException( 84 + status_code=400, detail="Already signed up for this activity") 85 + 86 + # Add student to participants 87 + result = activities_collection.update_one( 88 + {"_id": activity_name}, 89 + {"$push": {"participants": email}} 90 + ) 91 + 92 + if result.modified_count == 0: 93 + raise HTTPException(status_code=500, detail="Failed to update activity") 94 + 95 + return {"message": f"Signed up {email} for {activity_name}"} 96 + 97 + @router.post("/{activity_name}/unregister") 98 + def unregister_from_activity(activity_name: str, email: str, teacher_username: Optional[str] = Query(None)): 99 + """Remove a student from an activity - requires teacher authentication""" 100 + # Check teacher authentication 101 + if not teacher_username: 102 + raise HTTPException(status_code=401, detail="Authentication required for this action") 103 + 104 + teacher = teachers_collection.find_one({"_id": teacher_username}) 105 + if not teacher: 106 + raise HTTPException(status_code=401, detail="Invalid teacher credentials") 107 + 108 + # Get the activity 109 + activity = activities_collection.find_one({"_id": activity_name}) 110 + if not activity: 111 + raise HTTPException(status_code=404, detail="Activity not found") 112 + 113 + # Validate student is signed up 114 + if email not in activity["participants"]: 115 + raise HTTPException( 116 + status_code=400, detail="Not registered for this activity") 117 + 118 + # Remove student from participants 119 + result = activities_collection.update_one( 120 + {"_id": activity_name}, 121 + {"$pull": {"participants": email}} 122 + ) 123 + 124 + if result.modified_count == 0: 125 + raise HTTPException(status_code=500, detail="Failed to update activity") 126 + 127 + return {"message": f"Unregistered {email} from {activity_name}"}
+51
src/backend/routers/auth.py
··· 1 + """ 2 + Authentication endpoints for the High School Management System API 3 + """ 4 + 5 + from fastapi import APIRouter, HTTPException 6 + from typing import Dict, Any 7 + import hashlib 8 + 9 + from ..database import teachers_collection 10 + 11 + router = APIRouter( 12 + prefix="/auth", 13 + tags=["auth"] 14 + ) 15 + 16 + def hash_password(password): 17 + """Hash password using SHA-256""" 18 + return hashlib.sha256(password.encode()).hexdigest() 19 + 20 + @router.post("/login") 21 + def login(username: str, password: str) -> Dict[str, Any]: 22 + """Login a teacher account""" 23 + # Hash the provided password 24 + hashed_password = hash_password(password) 25 + 26 + # Find the teacher in the database 27 + teacher = teachers_collection.find_one({"_id": username}) 28 + 29 + if not teacher or teacher["password"] != hashed_password: 30 + raise HTTPException(status_code=401, detail="Invalid username or password") 31 + 32 + # Return teacher information (excluding password) 33 + return { 34 + "username": teacher["username"], 35 + "display_name": teacher["display_name"], 36 + "role": teacher["role"] 37 + } 38 + 39 + @router.get("/check-session") 40 + def check_session(username: str) -> Dict[str, Any]: 41 + """Check if a session is valid by username""" 42 + teacher = teachers_collection.find_one({"_id": username}) 43 + 44 + if not teacher: 45 + raise HTTPException(status_code=404, detail="Teacher not found") 46 + 47 + return { 48 + "username": teacher["username"], 49 + "display_name": teacher["display_name"], 50 + "role": teacher["role"] 51 + }
+868
src/static/app.js
··· 1 + document.addEventListener("DOMContentLoaded", () => { 2 + // DOM elements 3 + const activitiesList = document.getElementById("activities-list"); 4 + const messageDiv = document.getElementById("message"); 5 + const registrationModal = document.getElementById("registration-modal"); 6 + const modalActivityName = document.getElementById("modal-activity-name"); 7 + const signupForm = document.getElementById("signup-form"); 8 + const activityInput = document.getElementById("activity"); 9 + const closeRegistrationModal = document.querySelector(".close-modal"); 10 + 11 + // Search and filter elements 12 + const searchInput = document.getElementById("activity-search"); 13 + const searchButton = document.getElementById("search-button"); 14 + const categoryFilters = document.querySelectorAll(".category-filter"); 15 + const dayFilters = document.querySelectorAll(".day-filter"); 16 + const timeFilters = document.querySelectorAll(".time-filter"); 17 + 18 + // Authentication elements 19 + const loginButton = document.getElementById("login-button"); 20 + const userInfo = document.getElementById("user-info"); 21 + const displayName = document.getElementById("display-name"); 22 + const logoutButton = document.getElementById("logout-button"); 23 + const loginModal = document.getElementById("login-modal"); 24 + const loginForm = document.getElementById("login-form"); 25 + const closeLoginModal = document.querySelector(".close-login-modal"); 26 + const loginMessage = document.getElementById("login-message"); 27 + 28 + // Activity categories with corresponding colors 29 + const activityTypes = { 30 + sports: { label: "Sports", color: "#e8f5e9", textColor: "#2e7d32" }, 31 + arts: { label: "Arts", color: "#f3e5f5", textColor: "#7b1fa2" }, 32 + academic: { label: "Academic", color: "#e3f2fd", textColor: "#1565c0" }, 33 + community: { label: "Community", color: "#fff3e0", textColor: "#e65100" }, 34 + technology: { label: "Technology", color: "#e8eaf6", textColor: "#3949ab" }, 35 + }; 36 + 37 + // State for activities and filters 38 + let allActivities = {}; 39 + let currentFilter = "all"; 40 + let searchQuery = ""; 41 + let currentDay = ""; 42 + let currentTimeRange = ""; 43 + 44 + // Authentication state 45 + let currentUser = null; 46 + 47 + // Time range mappings for the dropdown 48 + const timeRanges = { 49 + morning: { start: "06:00", end: "08:00" }, // Before school hours 50 + afternoon: { start: "15:00", end: "18:00" }, // After school hours 51 + weekend: { days: ["Saturday", "Sunday"] }, // Weekend days 52 + }; 53 + 54 + // Initialize filters from active elements 55 + function initializeFilters() { 56 + // Initialize day filter 57 + const activeDayFilter = document.querySelector(".day-filter.active"); 58 + if (activeDayFilter) { 59 + currentDay = activeDayFilter.dataset.day; 60 + } 61 + 62 + // Initialize time filter 63 + const activeTimeFilter = document.querySelector(".time-filter.active"); 64 + if (activeTimeFilter) { 65 + currentTimeRange = activeTimeFilter.dataset.time; 66 + } 67 + } 68 + 69 + // Function to set day filter 70 + function setDayFilter(day) { 71 + currentDay = day; 72 + 73 + // Update active class 74 + dayFilters.forEach((btn) => { 75 + if (btn.dataset.day === day) { 76 + btn.classList.add("active"); 77 + } else { 78 + btn.classList.remove("active"); 79 + } 80 + }); 81 + 82 + fetchActivities(); 83 + } 84 + 85 + // Function to set time range filter 86 + function setTimeRangeFilter(timeRange) { 87 + currentTimeRange = timeRange; 88 + 89 + // Update active class 90 + timeFilters.forEach((btn) => { 91 + if (btn.dataset.time === timeRange) { 92 + btn.classList.add("active"); 93 + } else { 94 + btn.classList.remove("active"); 95 + } 96 + }); 97 + 98 + fetchActivities(); 99 + } 100 + 101 + // Check if user is already logged in (from localStorage) 102 + function checkAuthentication() { 103 + const savedUser = localStorage.getItem("currentUser"); 104 + if (savedUser) { 105 + try { 106 + currentUser = JSON.parse(savedUser); 107 + updateAuthUI(); 108 + // Verify the stored user with the server 109 + validateUserSession(currentUser.username); 110 + } catch (error) { 111 + console.error("Error parsing saved user", error); 112 + logout(); // Clear invalid data 113 + } 114 + } 115 + 116 + // Set authentication class on body 117 + updateAuthBodyClass(); 118 + } 119 + 120 + // Validate user session with the server 121 + async function validateUserSession(username) { 122 + try { 123 + const response = await fetch( 124 + `/auth/check-session?username=${encodeURIComponent(username)}` 125 + ); 126 + 127 + if (!response.ok) { 128 + // Session invalid, log out 129 + logout(); 130 + return; 131 + } 132 + 133 + // Session is valid, update user data 134 + const userData = await response.json(); 135 + currentUser = userData; 136 + localStorage.setItem("currentUser", JSON.stringify(userData)); 137 + updateAuthUI(); 138 + } catch (error) { 139 + console.error("Error validating session:", error); 140 + } 141 + } 142 + 143 + // Update UI based on authentication state 144 + function updateAuthUI() { 145 + if (currentUser) { 146 + loginButton.classList.add("hidden"); 147 + userInfo.classList.remove("hidden"); 148 + displayName.textContent = currentUser.display_name; 149 + } else { 150 + loginButton.classList.remove("hidden"); 151 + userInfo.classList.add("hidden"); 152 + displayName.textContent = ""; 153 + } 154 + 155 + updateAuthBodyClass(); 156 + // Refresh the activities to update the UI 157 + fetchActivities(); 158 + } 159 + 160 + // Update body class for CSS targeting 161 + function updateAuthBodyClass() { 162 + if (currentUser) { 163 + document.body.classList.remove("not-authenticated"); 164 + } else { 165 + document.body.classList.add("not-authenticated"); 166 + } 167 + } 168 + 169 + // Login function 170 + async function login(username, password) { 171 + try { 172 + const response = await fetch( 173 + `/auth/login?username=${encodeURIComponent( 174 + username 175 + )}&password=${encodeURIComponent(password)}`, 176 + { 177 + method: "POST", 178 + } 179 + ); 180 + 181 + const data = await response.json(); 182 + 183 + if (!response.ok) { 184 + showLoginMessage( 185 + data.detail || "Invalid username or password", 186 + "error" 187 + ); 188 + return false; 189 + } 190 + 191 + // Login successful 192 + currentUser = data; 193 + localStorage.setItem("currentUser", JSON.stringify(data)); 194 + updateAuthUI(); 195 + closeLoginModalHandler(); 196 + showMessage(`Welcome, ${currentUser.display_name}!`, "success"); 197 + return true; 198 + } catch (error) { 199 + console.error("Error during login:", error); 200 + showLoginMessage("Login failed. Please try again.", "error"); 201 + return false; 202 + } 203 + } 204 + 205 + // Logout function 206 + function logout() { 207 + currentUser = null; 208 + localStorage.removeItem("currentUser"); 209 + updateAuthUI(); 210 + showMessage("You have been logged out.", "info"); 211 + } 212 + 213 + // Show message in login modal 214 + function showLoginMessage(text, type) { 215 + loginMessage.textContent = text; 216 + loginMessage.className = `message ${type}`; 217 + loginMessage.classList.remove("hidden"); 218 + } 219 + 220 + // Open login modal 221 + function openLoginModal() { 222 + loginModal.classList.remove("hidden"); 223 + loginModal.classList.add("show"); 224 + loginMessage.classList.add("hidden"); 225 + loginForm.reset(); 226 + } 227 + 228 + // Close login modal 229 + function closeLoginModalHandler() { 230 + loginModal.classList.remove("show"); 231 + setTimeout(() => { 232 + loginModal.classList.add("hidden"); 233 + loginForm.reset(); 234 + }, 300); 235 + } 236 + 237 + // Event listeners for authentication 238 + loginButton.addEventListener("click", openLoginModal); 239 + logoutButton.addEventListener("click", logout); 240 + closeLoginModal.addEventListener("click", closeLoginModalHandler); 241 + 242 + // Close login modal when clicking outside 243 + window.addEventListener("click", (event) => { 244 + if (event.target === loginModal) { 245 + closeLoginModalHandler(); 246 + } 247 + }); 248 + 249 + // Handle login form submission 250 + loginForm.addEventListener("submit", async (event) => { 251 + event.preventDefault(); 252 + const username = document.getElementById("username").value; 253 + const password = document.getElementById("password").value; 254 + await login(username, password); 255 + }); 256 + 257 + // Show loading skeletons 258 + function showLoadingSkeletons() { 259 + activitiesList.innerHTML = ""; 260 + 261 + // Create more skeleton cards to fill the screen since they're smaller now 262 + for (let i = 0; i < 9; i++) { 263 + const skeletonCard = document.createElement("div"); 264 + skeletonCard.className = "skeleton-card"; 265 + skeletonCard.innerHTML = ` 266 + <div class="skeleton-line skeleton-title"></div> 267 + <div class="skeleton-line"></div> 268 + <div class="skeleton-line skeleton-text short"></div> 269 + <div style="margin-top: 8px;"> 270 + <div class="skeleton-line" style="height: 6px;"></div> 271 + <div class="skeleton-line skeleton-text short" style="height: 8px; margin-top: 3px;"></div> 272 + </div> 273 + <div style="margin-top: auto;"> 274 + <div class="skeleton-line" style="height: 24px; margin-top: 8px;"></div> 275 + </div> 276 + `; 277 + activitiesList.appendChild(skeletonCard); 278 + } 279 + } 280 + 281 + // Format schedule for display - handles both old and new format 282 + function formatSchedule(details) { 283 + // If schedule_details is available, use the structured data 284 + if (details.schedule_details) { 285 + const days = details.schedule_details.days.join(", "); 286 + 287 + // Convert 24h time format to 12h AM/PM format for display 288 + const formatTime = (time24) => { 289 + const [hours, minutes] = time24.split(":").map((num) => parseInt(num)); 290 + const period = hours >= 12 ? "PM" : "AM"; 291 + const displayHours = hours % 12 || 12; // Convert 0 to 12 for 12 AM 292 + return `${displayHours}:${minutes 293 + .toString() 294 + .padStart(2, "0")} ${period}`; 295 + }; 296 + 297 + const startTime = formatTime(details.schedule_details.start_time); 298 + const endTime = formatTime(details.schedule_details.end_time); 299 + 300 + return `${days}, ${startTime} - ${endTime}`; 301 + } 302 + 303 + // Fallback to the string format if schedule_details isn't available 304 + return details.schedule; 305 + } 306 + 307 + // Function to determine activity type (this would ideally come from backend) 308 + function getActivityType(activityName, description) { 309 + const name = activityName.toLowerCase(); 310 + const desc = description.toLowerCase(); 311 + 312 + if ( 313 + name.includes("soccer") || 314 + name.includes("basketball") || 315 + name.includes("sport") || 316 + name.includes("fitness") || 317 + desc.includes("team") || 318 + desc.includes("game") || 319 + desc.includes("athletic") 320 + ) { 321 + return "sports"; 322 + } else if ( 323 + name.includes("art") || 324 + name.includes("music") || 325 + name.includes("theater") || 326 + name.includes("drama") || 327 + desc.includes("creative") || 328 + desc.includes("paint") 329 + ) { 330 + return "arts"; 331 + } else if ( 332 + name.includes("science") || 333 + name.includes("math") || 334 + name.includes("academic") || 335 + name.includes("study") || 336 + name.includes("olympiad") || 337 + desc.includes("learning") || 338 + desc.includes("education") || 339 + desc.includes("competition") 340 + ) { 341 + return "academic"; 342 + } else if ( 343 + name.includes("volunteer") || 344 + name.includes("community") || 345 + desc.includes("service") || 346 + desc.includes("volunteer") 347 + ) { 348 + return "community"; 349 + } else if ( 350 + name.includes("computer") || 351 + name.includes("coding") || 352 + name.includes("tech") || 353 + name.includes("robotics") || 354 + desc.includes("programming") || 355 + desc.includes("technology") || 356 + desc.includes("digital") || 357 + desc.includes("robot") 358 + ) { 359 + return "technology"; 360 + } 361 + 362 + // Default to "academic" if no match 363 + return "academic"; 364 + } 365 + 366 + // Function to fetch activities from API with optional day and time filters 367 + async function fetchActivities() { 368 + // Show loading skeletons first 369 + showLoadingSkeletons(); 370 + 371 + try { 372 + // Build query string with filters if they exist 373 + let queryParams = []; 374 + 375 + // Handle day filter 376 + if (currentDay) { 377 + queryParams.push(`day=${encodeURIComponent(currentDay)}`); 378 + } 379 + 380 + // Handle time range filter 381 + if (currentTimeRange) { 382 + const range = timeRanges[currentTimeRange]; 383 + 384 + // Handle weekend special case 385 + if (currentTimeRange === "weekend") { 386 + // Don't add time parameters for weekend filter 387 + // Weekend filtering will be handled on the client side 388 + } else if (range) { 389 + // Add time parameters for before/after school 390 + queryParams.push(`start_time=${encodeURIComponent(range.start)}`); 391 + queryParams.push(`end_time=${encodeURIComponent(range.end)}`); 392 + } 393 + } 394 + 395 + const queryString = 396 + queryParams.length > 0 ? `?${queryParams.join("&")}` : ""; 397 + const response = await fetch(`/activities${queryString}`); 398 + const activities = await response.json(); 399 + 400 + // Save the activities data 401 + allActivities = activities; 402 + 403 + // Apply search and filter, and handle weekend filter in client 404 + displayFilteredActivities(); 405 + } catch (error) { 406 + activitiesList.innerHTML = 407 + "<p>Failed to load activities. Please try again later.</p>"; 408 + console.error("Error fetching activities:", error); 409 + } 410 + } 411 + 412 + // Function to display filtered activities 413 + function displayFilteredActivities() { 414 + // Clear the activities list 415 + activitiesList.innerHTML = ""; 416 + 417 + // Apply client-side filtering - this handles category filter and search, plus weekend filter 418 + let filteredActivities = {}; 419 + 420 + Object.entries(allActivities).forEach(([name, details]) => { 421 + const activityType = getActivityType(name, details.description); 422 + 423 + // Apply category filter 424 + if (currentFilter !== "all" && activityType !== currentFilter) { 425 + return; 426 + } 427 + 428 + // Apply weekend filter if selected 429 + if (currentTimeRange === "weekend" && details.schedule_details) { 430 + const activityDays = details.schedule_details.days; 431 + const isWeekendActivity = activityDays.some((day) => 432 + timeRanges.weekend.days.includes(day) 433 + ); 434 + 435 + if (!isWeekendActivity) { 436 + return; 437 + } 438 + } 439 + 440 + // Apply search filter 441 + const searchableContent = [ 442 + name.toLowerCase(), 443 + details.description.toLowerCase(), 444 + formatSchedule(details).toLowerCase(), 445 + ].join(" "); 446 + 447 + if ( 448 + searchQuery && 449 + !searchableContent.includes(searchQuery.toLowerCase()) 450 + ) { 451 + return; 452 + } 453 + 454 + // Activity passed all filters, add to filtered list 455 + filteredActivities[name] = details; 456 + }); 457 + 458 + // Check if there are any results 459 + if (Object.keys(filteredActivities).length === 0) { 460 + activitiesList.innerHTML = ` 461 + <div class="no-results"> 462 + <h4>No activities found</h4> 463 + <p>Try adjusting your search or filter criteria</p> 464 + </div> 465 + `; 466 + return; 467 + } 468 + 469 + // Display filtered activities 470 + Object.entries(filteredActivities).forEach(([name, details]) => { 471 + renderActivityCard(name, details); 472 + }); 473 + } 474 + 475 + // Function to render a single activity card 476 + function renderActivityCard(name, details) { 477 + const activityCard = document.createElement("div"); 478 + activityCard.className = "activity-card"; 479 + 480 + // Calculate spots and capacity 481 + const totalSpots = details.max_participants; 482 + const takenSpots = details.participants.length; 483 + const spotsLeft = totalSpots - takenSpots; 484 + const capacityPercentage = (takenSpots / totalSpots) * 100; 485 + const isFull = spotsLeft <= 0; 486 + 487 + // Determine capacity status class 488 + let capacityStatusClass = "capacity-available"; 489 + if (isFull) { 490 + capacityStatusClass = "capacity-full"; 491 + } else if (capacityPercentage >= 75) { 492 + capacityStatusClass = "capacity-near-full"; 493 + } 494 + 495 + // Determine activity type 496 + const activityType = getActivityType(name, details.description); 497 + const typeInfo = activityTypes[activityType]; 498 + 499 + // Format the schedule using the new helper function 500 + const formattedSchedule = formatSchedule(details); 501 + 502 + // Create activity tag 503 + const tagHtml = ` 504 + <span class="activity-tag" style="background-color: ${typeInfo.color}; color: ${typeInfo.textColor}"> 505 + ${typeInfo.label} 506 + </span> 507 + `; 508 + 509 + // Create capacity indicator 510 + const capacityIndicator = ` 511 + <div class="capacity-container ${capacityStatusClass}"> 512 + <div class="capacity-bar-bg"> 513 + <div class="capacity-bar-fill" style="width: ${capacityPercentage}%"></div> 514 + </div> 515 + <div class="capacity-text"> 516 + <span>${takenSpots} enrolled</span> 517 + <span>${spotsLeft} spots left</span> 518 + </div> 519 + </div> 520 + `; 521 + 522 + activityCard.innerHTML = ` 523 + ${tagHtml} 524 + <h4>${name}</h4> 525 + <p>${details.description}</p> 526 + <p class="tooltip"> 527 + <strong>Schedule:</strong> ${formattedSchedule} 528 + <span class="tooltip-text">Regular meetings at this time throughout the semester</span> 529 + </p> 530 + ${capacityIndicator} 531 + <div class="participants-list"> 532 + <h5>Current Participants:</h5> 533 + <ul> 534 + ${details.participants 535 + .map( 536 + (email) => ` 537 + <li> 538 + ${email} 539 + ${ 540 + currentUser 541 + ? ` 542 + <span class="delete-participant tooltip" data-activity="${name}" data-email="${email}"> 543 + 544 + <span class="tooltip-text">Unregister this student</span> 545 + </span> 546 + ` 547 + : "" 548 + } 549 + </li> 550 + ` 551 + ) 552 + .join("")} 553 + </ul> 554 + </div> 555 + <div class="activity-card-actions"> 556 + ${ 557 + currentUser 558 + ? ` 559 + <button class="register-button" data-activity="${name}" ${ 560 + isFull ? "disabled" : "" 561 + }> 562 + ${isFull ? "Activity Full" : "Register Student"} 563 + </button> 564 + ` 565 + : ` 566 + <div class="auth-notice"> 567 + Teachers can register students. 568 + </div> 569 + ` 570 + } 571 + </div> 572 + `; 573 + 574 + // Add click handlers for delete buttons 575 + const deleteButtons = activityCard.querySelectorAll(".delete-participant"); 576 + deleteButtons.forEach((button) => { 577 + button.addEventListener("click", handleUnregister); 578 + }); 579 + 580 + // Add click handler for register button (only when authenticated) 581 + if (currentUser) { 582 + const registerButton = activityCard.querySelector(".register-button"); 583 + if (!isFull) { 584 + registerButton.addEventListener("click", () => { 585 + openRegistrationModal(name); 586 + }); 587 + } 588 + } 589 + 590 + activitiesList.appendChild(activityCard); 591 + } 592 + 593 + // Event listeners for search and filter 594 + searchInput.addEventListener("input", (event) => { 595 + searchQuery = event.target.value; 596 + displayFilteredActivities(); 597 + }); 598 + 599 + searchButton.addEventListener("click", (event) => { 600 + event.preventDefault(); 601 + searchQuery = searchInput.value; 602 + displayFilteredActivities(); 603 + }); 604 + 605 + // Add event listeners to category filter buttons 606 + categoryFilters.forEach((button) => { 607 + button.addEventListener("click", () => { 608 + // Update active class 609 + categoryFilters.forEach((btn) => btn.classList.remove("active")); 610 + button.classList.add("active"); 611 + 612 + // Update current filter and display filtered activities 613 + currentFilter = button.dataset.category; 614 + displayFilteredActivities(); 615 + }); 616 + }); 617 + 618 + // Add event listeners to day filter buttons 619 + dayFilters.forEach((button) => { 620 + button.addEventListener("click", () => { 621 + // Update active class 622 + dayFilters.forEach((btn) => btn.classList.remove("active")); 623 + button.classList.add("active"); 624 + 625 + // Update current day filter and fetch activities 626 + currentDay = button.dataset.day; 627 + fetchActivities(); 628 + }); 629 + }); 630 + 631 + // Add event listeners for time filter buttons 632 + timeFilters.forEach((button) => { 633 + button.addEventListener("click", () => { 634 + // Update active class 635 + timeFilters.forEach((btn) => btn.classList.remove("active")); 636 + button.classList.add("active"); 637 + 638 + // Update current time filter and fetch activities 639 + currentTimeRange = button.dataset.time; 640 + fetchActivities(); 641 + }); 642 + }); 643 + 644 + // Open registration modal 645 + function openRegistrationModal(activityName) { 646 + modalActivityName.textContent = activityName; 647 + activityInput.value = activityName; 648 + registrationModal.classList.remove("hidden"); 649 + // Add slight delay to trigger animation 650 + setTimeout(() => { 651 + registrationModal.classList.add("show"); 652 + }, 10); 653 + } 654 + 655 + // Close registration modal 656 + function closeRegistrationModalHandler() { 657 + registrationModal.classList.remove("show"); 658 + setTimeout(() => { 659 + registrationModal.classList.add("hidden"); 660 + signupForm.reset(); 661 + }, 300); 662 + } 663 + 664 + // Event listener for close button 665 + closeRegistrationModal.addEventListener( 666 + "click", 667 + closeRegistrationModalHandler 668 + ); 669 + 670 + // Close modal when clicking outside of it 671 + window.addEventListener("click", (event) => { 672 + if (event.target === registrationModal) { 673 + closeRegistrationModalHandler(); 674 + } 675 + }); 676 + 677 + // Create and show confirmation dialog 678 + function showConfirmationDialog(message, confirmCallback) { 679 + // Create the confirmation dialog if it doesn't exist 680 + let confirmDialog = document.getElementById("confirm-dialog"); 681 + if (!confirmDialog) { 682 + confirmDialog = document.createElement("div"); 683 + confirmDialog.id = "confirm-dialog"; 684 + confirmDialog.className = "modal hidden"; 685 + confirmDialog.innerHTML = ` 686 + <div class="modal-content"> 687 + <h3>Confirm Action</h3> 688 + <p id="confirm-message"></p> 689 + <div style="display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px;"> 690 + <button id="cancel-button" class="cancel-btn">Cancel</button> 691 + <button id="confirm-button" class="confirm-btn">Confirm</button> 692 + </div> 693 + </div> 694 + `; 695 + document.body.appendChild(confirmDialog); 696 + 697 + // Style the buttons 698 + const cancelBtn = confirmDialog.querySelector("#cancel-button"); 699 + const confirmBtn = confirmDialog.querySelector("#confirm-button"); 700 + 701 + cancelBtn.style.backgroundColor = "#f1f1f1"; 702 + cancelBtn.style.color = "#333"; 703 + 704 + confirmBtn.style.backgroundColor = "#dc3545"; 705 + confirmBtn.style.color = "white"; 706 + } 707 + 708 + // Set the message 709 + const confirmMessage = document.getElementById("confirm-message"); 710 + confirmMessage.textContent = message; 711 + 712 + // Show the dialog 713 + confirmDialog.classList.remove("hidden"); 714 + setTimeout(() => { 715 + confirmDialog.classList.add("show"); 716 + }, 10); 717 + 718 + // Handle button clicks 719 + const cancelButton = document.getElementById("cancel-button"); 720 + const confirmButton = document.getElementById("confirm-button"); 721 + 722 + // Remove any existing event listeners 723 + const newCancelButton = cancelButton.cloneNode(true); 724 + const newConfirmButton = confirmButton.cloneNode(true); 725 + cancelButton.parentNode.replaceChild(newCancelButton, cancelButton); 726 + confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton); 727 + 728 + // Add new event listeners 729 + newCancelButton.addEventListener("click", () => { 730 + confirmDialog.classList.remove("show"); 731 + setTimeout(() => { 732 + confirmDialog.classList.add("hidden"); 733 + }, 300); 734 + }); 735 + 736 + newConfirmButton.addEventListener("click", () => { 737 + confirmCallback(); 738 + confirmDialog.classList.remove("show"); 739 + setTimeout(() => { 740 + confirmDialog.classList.add("hidden"); 741 + }, 300); 742 + }); 743 + 744 + // Close when clicking outside 745 + confirmDialog.addEventListener("click", (event) => { 746 + if (event.target === confirmDialog) { 747 + confirmDialog.classList.remove("show"); 748 + setTimeout(() => { 749 + confirmDialog.classList.add("hidden"); 750 + }, 300); 751 + } 752 + }); 753 + } 754 + 755 + // Handle unregistration with confirmation 756 + async function handleUnregister(event) { 757 + // Check if user is authenticated 758 + if (!currentUser) { 759 + showMessage( 760 + "You must be logged in as a teacher to unregister students.", 761 + "error" 762 + ); 763 + return; 764 + } 765 + 766 + const activity = event.target.dataset.activity; 767 + const email = event.target.dataset.email; 768 + 769 + // Show confirmation dialog 770 + showConfirmationDialog( 771 + `Are you sure you want to unregister ${email} from ${activity}?`, 772 + async () => { 773 + try { 774 + const response = await fetch( 775 + `/activities/${encodeURIComponent( 776 + activity 777 + )}/unregister?email=${encodeURIComponent( 778 + email 779 + )}&teacher_username=${encodeURIComponent(currentUser.username)}`, 780 + { 781 + method: "POST", 782 + } 783 + ); 784 + 785 + const result = await response.json(); 786 + 787 + if (response.ok) { 788 + showMessage(result.message, "success"); 789 + // Refresh the activities list 790 + fetchActivities(); 791 + } else { 792 + showMessage(result.detail || "An error occurred", "error"); 793 + } 794 + } catch (error) { 795 + showMessage("Failed to unregister. Please try again.", "error"); 796 + console.error("Error unregistering:", error); 797 + } 798 + } 799 + ); 800 + } 801 + 802 + // Show message function 803 + function showMessage(text, type) { 804 + messageDiv.textContent = text; 805 + messageDiv.className = `message ${type}`; 806 + messageDiv.classList.remove("hidden"); 807 + 808 + // Hide message after 5 seconds 809 + setTimeout(() => { 810 + messageDiv.classList.add("hidden"); 811 + }, 5000); 812 + } 813 + 814 + // Handle form submission 815 + signupForm.addEventListener("submit", async (event) => { 816 + event.preventDefault(); 817 + 818 + // Check if user is authenticated 819 + if (!currentUser) { 820 + showMessage( 821 + "You must be logged in as a teacher to register students.", 822 + "error" 823 + ); 824 + return; 825 + } 826 + 827 + const email = document.getElementById("email").value; 828 + const activity = activityInput.value; 829 + 830 + try { 831 + const response = await fetch( 832 + `/activities/${encodeURIComponent( 833 + activity 834 + )}/signup?email=${encodeURIComponent( 835 + email 836 + )}&teacher_username=${encodeURIComponent(currentUser.username)}`, 837 + { 838 + method: "POST", 839 + } 840 + ); 841 + 842 + const result = await response.json(); 843 + 844 + if (response.ok) { 845 + showMessage(result.message, "success"); 846 + closeRegistrationModalHandler(); 847 + // Refresh the activities list after successful signup 848 + fetchActivities(); 849 + } else { 850 + showMessage(result.detail || "An error occurred", "error"); 851 + } 852 + } catch (error) { 853 + showMessage("Failed to sign up. Please try again.", "error"); 854 + console.error("Error signing up:", error); 855 + } 856 + }); 857 + 858 + // Expose filter functions to window for future UI control 859 + window.activityFilters = { 860 + setDayFilter, 861 + setTimeRangeFilter, 862 + }; 863 + 864 + // Initialize app 865 + checkAuthentication(); 866 + initializeFilters(); 867 + fetchActivities(); 868 + });
+173
src/static/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 + <title>Mergington High School Activities</title> 7 + <link rel="stylesheet" href="styles.css" /> 8 + </head> 9 + <body> 10 + <header> 11 + <h1>Mergington High School</h1> 12 + <h2>Extracurricular Activities</h2> 13 + <div id="user-controls"> 14 + <div id="user-status"> 15 + <button id="login-button" class="icon-button"> 16 + <span class="user-icon">👤</span> 17 + <span>Login</span> 18 + </button> 19 + <div id="user-info" class="hidden"> 20 + <span id="display-name"></span> 21 + <button id="logout-button">Logout</button> 22 + </div> 23 + </div> 24 + </div> 25 + </header> 26 + 27 + <main> 28 + <section id="activities-container"> 29 + <div class="main-content-layout"> 30 + <!-- Left Sidebar for Filters --> 31 + <aside class="sidebar-filters"> 32 + <h3>Filter Activities</h3> 33 + <!-- Search Box --> 34 + <div class="search-box"> 35 + <input 36 + type="text" 37 + id="activity-search" 38 + placeholder="Search activities..." 39 + /> 40 + <button id="search-button" aria-label="Search"> 41 + <span class="search-icon">🔍</span> 42 + </button> 43 + </div> 44 + 45 + <div class="filter-container"> 46 + <div class="filter-label">Filter by category:</div> 47 + <div class="category-filters" id="category-filters"> 48 + <button class="category-filter active" data-category="all"> 49 + All 50 + </button> 51 + <button class="category-filter" data-category="sports"> 52 + Sports 53 + </button> 54 + <button class="category-filter" data-category="arts"> 55 + Arts 56 + </button> 57 + <button class="category-filter" data-category="academic"> 58 + Academic 59 + </button> 60 + <button class="category-filter" data-category="community"> 61 + Community 62 + </button> 63 + <button class="category-filter" data-category="technology"> 64 + Technology 65 + </button> 66 + </div> 67 + </div> 68 + 69 + <!-- Day Filter --> 70 + <div class="filter-container day-filter-container"> 71 + <div class="filter-label">Filter by day:</div> 72 + <div class="day-filters" id="day-filters"> 73 + <button class="day-filter active" data-day="">All Days</button> 74 + <button class="day-filter" data-day="Monday">Monday</button> 75 + <button class="day-filter" data-day="Tuesday">Tuesday</button> 76 + <button class="day-filter" data-day="Wednesday"> 77 + Wednesday 78 + </button> 79 + <button class="day-filter" data-day="Thursday">Thursday</button> 80 + <button class="day-filter" data-day="Friday">Friday</button> 81 + <button class="day-filter" data-day="Saturday">Saturday</button> 82 + <button class="day-filter" data-day="Sunday">Sunday</button> 83 + </div> 84 + </div> 85 + 86 + <!-- Time Filter --> 87 + <div class="filter-container time-filter-container"> 88 + <div class="filter-label">Filter by time:</div> 89 + <div class="time-filters"> 90 + <button class="time-filter active" data-time=""> 91 + All Times 92 + </button> 93 + <button class="time-filter" data-time="morning"> 94 + Before School 95 + </button> 96 + <button class="time-filter" data-time="afternoon"> 97 + After School 98 + </button> 99 + <button class="time-filter" data-time="weekend">Weekend</button> 100 + </div> 101 + </div> 102 + </aside> 103 + 104 + <!-- Activities Content --> 105 + <div class="activities-content"> 106 + <div id="activities-list"> 107 + <!-- Activities will be loaded here --> 108 + <p>Loading activities...</p> 109 + </div> 110 + <div id="message" class="hidden message"></div> 111 + </div> 112 + </div> 113 + </section> 114 + </main> 115 + 116 + <footer> 117 + <p>&copy; 2023 Mergington High School</p> 118 + </footer> 119 + 120 + <!-- Registration Modal --> 121 + <div id="registration-modal" class="modal hidden"> 122 + <div class="modal-content"> 123 + <span class="close-modal">&times;</span> 124 + <h3>Register for <span id="modal-activity-name"></span></h3> 125 + <form id="signup-form"> 126 + <div class="form-group"> 127 + <label for="email">Student Email:</label> 128 + <input 129 + type="email" 130 + id="email" 131 + required 132 + placeholder="your-email@mergington.edu" 133 + /> 134 + </div> 135 + <input type="hidden" id="activity" value="" /> 136 + <button type="submit">Register</button> 137 + </form> 138 + </div> 139 + </div> 140 + 141 + <!-- Login Modal --> 142 + <div id="login-modal" class="modal hidden"> 143 + <div class="modal-content"> 144 + <span class="close-login-modal">&times;</span> 145 + <h3>Teacher Login</h3> 146 + <form id="login-form"> 147 + <div class="form-group"> 148 + <label for="username">Username:</label> 149 + <input 150 + type="text" 151 + id="username" 152 + required 153 + placeholder="Enter your username" 154 + /> 155 + </div> 156 + <div class="form-group"> 157 + <label for="password">Password:</label> 158 + <input 159 + type="password" 160 + id="password" 161 + required 162 + placeholder="Enter your password" 163 + /> 164 + </div> 165 + <button type="submit">Login</button> 166 + </form> 167 + <div id="login-message" class="hidden message"></div> 168 + </div> 169 + </div> 170 + 171 + <script src="app.js"></script> 172 + </body> 173 + </html>
+666
src/static/styles.css
··· 1 + /* Color palette */ 2 + :root { 3 + /* Primary colors */ 4 + --primary: #1a237e; 5 + --primary-light: #534bae; 6 + --primary-dark: #000051; 7 + --primary-text: #ffffff; 8 + 9 + /* Secondary colors */ 10 + --secondary: #ff6f00; 11 + --secondary-light: #ffa040; 12 + --secondary-dark: #c43e00; 13 + --secondary-text: #ffffff; 14 + 15 + /* Neutral colors */ 16 + --background: #f5f5f5; 17 + --surface: #ffffff; 18 + --text-primary: #333333; 19 + --text-secondary: #666666; 20 + --border: #e0e0e0; 21 + --border-light: #f0f0f0; 22 + --border-focus: #d0d0d0; 23 + 24 + /* Feedback colors */ 25 + --success: #2e7d32; 26 + --success-light: #e8f5e9; 27 + --warning: #ff9800; 28 + --warning-light: #fff3cd; 29 + --error: #c62828; 30 + --error-light: #ffebee; 31 + --info: #0c5460; 32 + --info-light: #d1ecf1; 33 + } 34 + 35 + * { 36 + box-sizing: border-box; 37 + margin: 0; 38 + padding: 0; 39 + font-family: Arial, sans-serif; 40 + } 41 + 42 + body { 43 + font-family: Arial, sans-serif; 44 + line-height: 1.4; 45 + color: var(--text-primary); 46 + max-width: 1200px; 47 + margin: 0 auto; 48 + padding: 12px; 49 + background-color: var(--background); 50 + font-size: 0.9rem; 51 + } 52 + 53 + header { 54 + text-align: center; 55 + padding: 12px 0; 56 + margin-bottom: 15px; 57 + background-color: var(--primary); 58 + color: var(--primary-text); 59 + border-radius: 5px; 60 + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16); 61 + position: relative; 62 + display: flex; 63 + flex-direction: column; 64 + align-items: center; 65 + } 66 + 67 + header h1 { 68 + margin-bottom: 5px; 69 + font-size: 1.6rem; 70 + } 71 + 72 + header h2 { 73 + font-size: 1.2rem; 74 + } 75 + 76 + main { 77 + display: flex; 78 + flex-wrap: wrap; 79 + justify-content: center; 80 + } 81 + 82 + section { 83 + background-color: var(--surface); 84 + padding: 15px; 85 + border-radius: 5px; 86 + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 87 + width: 100%; 88 + } 89 + 90 + section h3 { 91 + margin-bottom: 15px; 92 + padding-bottom: 8px; 93 + border-bottom: 1px solid var(--border); 94 + color: var(--primary); 95 + font-size: 1.1rem; 96 + } 97 + 98 + /* New Layout Styles */ 99 + .main-content-layout { 100 + display: flex; 101 + flex-direction: column; 102 + gap: 15px; 103 + } 104 + 105 + .sidebar-filters { 106 + padding: 15px; 107 + background-color: var(--surface); 108 + border-radius: 5px; 109 + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); 110 + } 111 + 112 + .sidebar-filters h3 { 113 + margin-bottom: 15px; 114 + padding-bottom: 8px; 115 + border-bottom: 1px solid var(--border); 116 + color: var(--primary); 117 + font-size: 1.1rem; 118 + } 119 + 120 + .activities-content { 121 + flex: 1; 122 + } 123 + 124 + /* Desktop layout */ 125 + @media (min-width: 768px) { 126 + .main-content-layout { 127 + flex-direction: row; 128 + align-items: flex-start; 129 + } 130 + 131 + .sidebar-filters { 132 + width: 200px; /* Reduced from 250px to 200px to make the sidebar narrower */ 133 + position: sticky; 134 + top: 15px; 135 + max-height: calc(100vh - 30px); 136 + overflow-y: auto; 137 + } 138 + 139 + .activities-content { 140 + flex: 1; 141 + } 142 + } 143 + 144 + #activities-list { 145 + display: grid; 146 + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 147 + gap: 15px; 148 + width: 100%; 149 + } 150 + 151 + .activity-card { 152 + padding: 12px; 153 + border: 1px solid var(--border); 154 + border-radius: 6px; 155 + background-color: var(--surface); 156 + display: flex; 157 + flex-direction: column; 158 + height: 100%; 159 + transition: all 0.3s ease; 160 + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); 161 + position: relative; 162 + overflow: hidden; 163 + font-size: 0.85rem; 164 + } 165 + 166 + .activity-card:hover { 167 + transform: translateY(-5px); 168 + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1); 169 + border-color: var(--border-focus); 170 + } 171 + 172 + .activity-card h4 { 173 + margin-bottom: 8px; 174 + color: var(--primary); 175 + font-size: 1rem; 176 + letter-spacing: 0.3px; 177 + padding-bottom: 6px; 178 + border-bottom: 1px solid var(--border-light); 179 + } 180 + 181 + .activity-card p { 182 + margin-bottom: 8px; 183 + line-height: 1.4; 184 + } 185 + 186 + .activity-card-actions { 187 + margin-top: auto; 188 + padding-top: 10px; 189 + display: flex; 190 + justify-content: center; 191 + } 192 + 193 + /* Activity Tag */ 194 + .activity-tag { 195 + position: absolute; 196 + top: 8px; 197 + right: 8px; 198 + background: #e8eaf6; 199 + color: #3949ab; 200 + font-size: 0.65rem; 201 + font-weight: bold; 202 + padding: 2px 6px; 203 + border-radius: 10px; 204 + text-transform: uppercase; 205 + letter-spacing: 0.3px; 206 + } 207 + 208 + /* Capacity Indicator */ 209 + .capacity-container { 210 + margin: 8px 0; 211 + width: 100%; 212 + } 213 + 214 + .capacity-bar-bg { 215 + height: 6px; 216 + background-color: var(--border-light); 217 + border-radius: 3px; 218 + overflow: hidden; 219 + } 220 + 221 + .capacity-text { 222 + display: flex; 223 + justify-content: space-between; 224 + margin-top: 3px; 225 + font-size: 0.7rem; 226 + color: var(--text-secondary); 227 + } 228 + 229 + .capacity-full .capacity-bar-fill { 230 + background-color: var(--error); 231 + } 232 + 233 + .capacity-near-full .capacity-bar-fill { 234 + background-color: var(--warning); 235 + } 236 + 237 + .capacity-available .capacity-bar-fill { 238 + background-color: var(--success); 239 + } 240 + 241 + /* Participants list */ 242 + .participants-list { 243 + margin-top: 8px; 244 + padding-top: 8px; 245 + border-top: 1px solid var(--border-light); 246 + } 247 + 248 + .participants-list h5 { 249 + color: var(--primary); 250 + margin-bottom: 5px; 251 + font-size: 0.8em; 252 + } 253 + 254 + .participants-list ul { 255 + list-style-type: none; 256 + padding-left: 0; 257 + margin: 0; 258 + max-height: 100px; 259 + } 260 + 261 + .participants-list li { 262 + padding: 2px 0; 263 + color: var(--text-secondary); 264 + font-size: 0.8em; 265 + display: flex; 266 + justify-content: space-between; 267 + align-items: center; 268 + } 269 + 270 + /* Buttons */ 271 + .register-button { 272 + background: linear-gradient(145deg, var(--secondary), var(--secondary-dark)); 273 + color: var(--secondary-text); 274 + margin-top: 10px; 275 + padding: 6px 12px; 276 + width: 100%; 277 + font-weight: bold; 278 + border-radius: 20px; 279 + box-shadow: 0 2px 4px rgba(255, 111, 0, 0.2); 280 + transition: all 0.3s ease; 281 + display: flex; 282 + justify-content: center; 283 + align-items: center; 284 + font-size: 0.85rem; 285 + letter-spacing: 0.3px; 286 + text-transform: uppercase; 287 + border: none; 288 + } 289 + 290 + button { 291 + background-color: var(--primary); 292 + color: white; 293 + border: none; 294 + padding: 6px 12px; 295 + font-size: 0.85rem; 296 + border-radius: 4px; 297 + cursor: pointer; 298 + transition: background-color 0.2s; 299 + } 300 + 301 + /* Tooltip styles */ 302 + .tooltip { 303 + position: relative; 304 + display: inline-block; 305 + cursor: pointer; 306 + } 307 + 308 + .tooltip .tooltip-text { 309 + visibility: hidden; 310 + background-color: rgba(33, 33, 33, 0.9); 311 + color: #fff; 312 + text-align: center; 313 + padding: 8px 12px; 314 + border-radius: 4px; 315 + font-size: 0.8rem; 316 + position: absolute; 317 + z-index: 1; 318 + bottom: 125%; 319 + left: 50%; 320 + transform: translateX(-50%); 321 + opacity: 0; 322 + transition: opacity 0.2s, visibility 0.2s; 323 + width: max-content; 324 + max-width: 250px; 325 + pointer-events: none; 326 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 327 + } 328 + 329 + .tooltip:hover .tooltip-text { 330 + visibility: visible; 331 + opacity: 1; 332 + } 333 + 334 + /* Special positioning for delete participant tooltip */ 335 + .delete-participant { 336 + cursor: pointer; 337 + } 338 + 339 + .delete-participant .tooltip-text { 340 + left: auto; 341 + right: calc(100% + 8px); 342 + top: 50%; 343 + bottom: auto; 344 + transform: translateY(-50%); 345 + white-space: nowrap; 346 + } 347 + 348 + /* Activity skeletons for loading state */ 349 + .skeleton-card { 350 + padding: 12px; 351 + border: 1px solid var(--border); 352 + border-radius: 6px; 353 + background-color: var(--surface); 354 + display: flex; 355 + flex-direction: column; 356 + height: 180px; 357 + position: relative; 358 + overflow: hidden; 359 + } 360 + 361 + .skeleton-line { 362 + height: 10px; 363 + margin-bottom: 8px; 364 + background: linear-gradient( 365 + 90deg, 366 + var(--border-light) 25%, 367 + var(--border) 50%, 368 + var(--border-light) 75% 369 + ); 370 + background-size: 200% 100%; 371 + border-radius: 3px; 372 + animation: shimmer 1.5s infinite; 373 + } 374 + 375 + .skeleton-title { 376 + height: 18px; 377 + width: 70%; 378 + margin-bottom: 10px; 379 + } 380 + 381 + .skeleton-text { 382 + width: 100%; 383 + } 384 + 385 + .skeleton-text.short { 386 + width: 60%; 387 + } 388 + 389 + @keyframes shimmer { 390 + 0% { 391 + background-position: -200% 0; 392 + } 393 + 100% { 394 + background-position: 200% 0; 395 + } 396 + } 397 + 398 + /* Modal animation */ 399 + .modal { 400 + position: fixed; 401 + top: 0; 402 + left: 0; 403 + width: 100%; 404 + height: 100%; 405 + background-color: rgba(0, 0, 0, 0.5); 406 + display: flex; 407 + justify-content: center; 408 + align-items: center; 409 + z-index: 1000; 410 + opacity: 0; 411 + transition: opacity 0.3s ease; 412 + } 413 + 414 + .modal.show { 415 + opacity: 1; 416 + } 417 + 418 + .modal-content { 419 + background-color: var(--surface); 420 + padding: 18px; 421 + border-radius: 5px; 422 + width: 90%; 423 + max-width: 350px; 424 + position: relative; 425 + transform: translateY(-20px); 426 + transition: transform 0.3s ease; 427 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); 428 + border: 1px solid var(--border); 429 + } 430 + 431 + .modal.show .modal-content { 432 + transform: translateY(0); 433 + } 434 + 435 + .close-modal, 436 + .close-login-modal { 437 + position: absolute; 438 + right: 12px; 439 + top: 8px; 440 + font-size: 18px; 441 + cursor: pointer; 442 + color: var(--text-secondary); 443 + line-height: 1; 444 + } 445 + 446 + /* Login modal specific styling */ 447 + #login-modal .modal-content h3 { 448 + color: var(--primary); 449 + font-size: 1.2rem; 450 + margin-bottom: 15px; 451 + text-align: center; 452 + } 453 + 454 + /* Hidden class - critical for showing/hiding elements */ 455 + .hidden { 456 + display: none !important; 457 + } 458 + 459 + /* More compact no-results message */ 460 + .no-results { 461 + text-align: center; 462 + padding: 12px; 463 + color: var(--text-secondary); 464 + background-color: var(--surface); 465 + border-radius: 6px; 466 + border: 1px dashed var(--border); 467 + margin: 10px 0; 468 + font-size: 0.85rem; 469 + } 470 + 471 + footer { 472 + text-align: center; 473 + margin-top: 20px; 474 + padding: 10px; 475 + color: var(--text-secondary); 476 + font-size: 0.8rem; 477 + } 478 + 479 + /* Search and Filter Components - Updated for Sidebar */ 480 + .search-box { 481 + display: flex; 482 + width: 100%; 483 + margin-bottom: 15px; 484 + } 485 + 486 + .search-box input { 487 + flex: 1; 488 + min-width: 0; /* Add this to prevent input from overflowing */ 489 + padding: 6px 10px; 490 + border: 1px solid var(--border); 491 + border-right: none; 492 + border-radius: 20px 0 0 20px; 493 + font-size: 0.85rem; 494 + transition: all 0.3s ease; 495 + } 496 + 497 + .search-box input:focus { 498 + outline: none; 499 + border-color: var(--primary-light); 500 + box-shadow: 0 0 0 2px rgba(83, 75, 174, 0.1); 501 + } 502 + 503 + .search-box button { 504 + width: 36px; 505 + height: 100%; /* Make button match input height */ 506 + min-height: 32px; /* Ensure minimum clickable area */ 507 + background: var(--primary); 508 + color: white; 509 + border: none; 510 + border-radius: 0 20px 20px 0; 511 + cursor: pointer; 512 + transition: background-color 0.3s ease; 513 + padding: 0; 514 + display: flex; 515 + align-items: center; 516 + justify-content: center; 517 + } 518 + 519 + .search-box button:hover { 520 + background-color: var(--primary-light); 521 + } 522 + 523 + .search-icon { 524 + font-size: 1rem; 525 + } 526 + 527 + .filter-container { 528 + margin-bottom: 15px; 529 + display: flex; 530 + flex-direction: column; 531 + gap: 6px; 532 + } 533 + 534 + .filter-label { 535 + font-weight: 600; 536 + color: var(--text-primary); 537 + font-size: 0.8rem; 538 + } 539 + 540 + .category-filters, 541 + .day-filters, 542 + .time-filters { 543 + display: flex; 544 + flex-wrap: wrap; 545 + gap: 6px; 546 + } 547 + 548 + .category-filter, 549 + .day-filter, 550 + .time-filter { 551 + background-color: var(--background); 552 + border: 1px solid var(--border); 553 + color: var(--text-primary); 554 + padding: 4px 10px; 555 + border-radius: 15px; 556 + font-size: 0.75rem; 557 + cursor: pointer; 558 + transition: all 0.2s ease; 559 + } 560 + 561 + .category-filter.active, 562 + .day-filter.active, 563 + .time-filter.active { 564 + background-color: var(--primary); 565 + color: white; 566 + border-color: var(--primary-dark); 567 + } 568 + 569 + .category-filter:hover, 570 + .day-filter:hover, 571 + .time-filter:hover { 572 + background-color: var(--primary-light); 573 + color: white; 574 + } 575 + 576 + .time-filters { 577 + width: 100%; 578 + } 579 + 580 + .reset-button { 581 + background-color: var(--border); 582 + color: var(--text-primary); 583 + padding: 5px 12px; 584 + border-radius: 15px; 585 + font-size: 0.8rem; 586 + font-weight: 500; 587 + cursor: pointer; 588 + transition: all 0.2s ease; 589 + align-self: flex-start; 590 + } 591 + 592 + .reset-button:hover { 593 + background-color: var(--border-focus); 594 + } 595 + 596 + /* Responsive adjustments for mobile */ 597 + @media (max-width: 767px) { 598 + .sidebar-filters { 599 + margin-bottom: 15px; 600 + } 601 + 602 + .main-content-layout { 603 + flex-direction: column; 604 + } 605 + 606 + #activities-list { 607 + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 608 + } 609 + 610 + .sidebar-filters { 611 + width: 100%; 612 + } 613 + } 614 + 615 + /* User controls in header */ 616 + #user-controls { 617 + position: absolute; 618 + top: 10px; 619 + right: 15px; 620 + } 621 + 622 + #user-status { 623 + display: flex; 624 + align-items: center; 625 + } 626 + 627 + #user-info { 628 + display: flex; 629 + align-items: center; 630 + gap: 10px; 631 + } 632 + 633 + #display-name { 634 + margin-right: 5px; 635 + font-weight: 500; 636 + } 637 + 638 + .icon-button { 639 + display: flex; 640 + align-items: center; 641 + gap: 5px; 642 + background-color: rgba(255, 255, 255, 0.2); 643 + border-radius: 20px; 644 + padding: 4px 12px; 645 + font-size: 0.85rem; 646 + transition: background-color 0.2s; 647 + } 648 + 649 + .icon-button:hover { 650 + background-color: rgba(255, 255, 255, 0.3); 651 + } 652 + 653 + .user-icon { 654 + font-size: 1rem; 655 + } 656 + 657 + #logout-button { 658 + padding: 3px 10px; 659 + background-color: rgba(255, 255, 255, 0.2); 660 + font-size: 0.8rem; 661 + border-radius: 20px; 662 + } 663 + 664 + #logout-button:hover { 665 + background-color: rgba(255, 255, 255, 0.3); 666 + }