├── .dockerignore ├── .flake8 ├── .github └── workflows │ ├── backend_lint.yml │ ├── deploy.yml │ └── front_end_eslint.yml ├── .gitignore ├── .gitpod.yml ├── CONTRIBUTING.md ├── Contributors.md ├── LICENSE.md ├── README.md ├── ansible ├── README.md ├── ansible.cfg ├── deploy.yaml ├── hosts ├── roles │ ├── common │ │ └── tasks │ │ │ └── main.yaml │ ├── docker │ │ └── tasks │ │ │ └── main.yaml │ └── security │ │ └── tasks │ │ └── main.yaml └── setup.yaml ├── backend ├── Dockerfile ├── accounts │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_profile.py │ │ ├── 0003_alter_profile_follows.py │ │ ├── 0004_alter_profile_options.py │ │ └── __init__.py │ ├── models.py │ └── signals.py ├── api │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── chitchat │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── core │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_rename_user_post_user_profile.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── db.sqlite3.bak ├── manage.py ├── pyproject.toml └── requirements.txt ├── docker-compose.yaml ├── documentation └── images │ ├── clipboard-screenshot.png │ ├── clone-screenshot.png │ └── fork-screenshot.png ├── frontend ├── .eslintrc.json ├── .prettierrc.json ├── Dockerfile ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── user.png │ └── vite.svg ├── src │ ├── App.jsx │ ├── assets │ │ ├── cc1.jpeg │ │ ├── cc2.ico │ │ ├── cc2.jpeg │ │ ├── cc3.jpeg │ │ ├── cc4 - Copy.jpeg │ │ ├── cc4.jpeg │ │ ├── chitchat-logo.png │ │ ├── profile.jpg │ │ ├── react.svg │ │ └── user.png │ ├── components │ │ ├── Home │ │ │ ├── CreatePost.tsx │ │ │ ├── MainCanvas.tsx │ │ │ ├── SideBar.tsx │ │ │ ├── Suggested.tsx │ │ │ └── TrendingTopics.tsx │ │ ├── NavBar │ │ │ ├── NavBar.tsx │ │ │ ├── NavButtons.tsx │ │ │ └── NavSearch.tsx │ │ ├── Profile │ │ │ ├── Followers.tsx │ │ │ ├── Following.tsx │ │ │ ├── Loaders │ │ │ │ ├── FollowerSkeletonLoader.tsx │ │ │ │ ├── HomeProfileSkeleton.tsx │ │ │ │ ├── PostsSkeleton.tsx │ │ │ │ └── ProfileSkeletonLoader.tsx │ │ │ ├── Posts.tsx │ │ │ └── UserProfileCard.tsx │ │ ├── auth │ │ │ └── index.jsx │ │ ├── common │ │ │ ├── Button.tsx │ │ │ ├── Post.tsx │ │ │ ├── Trending.tsx │ │ │ └── UserFollow.tsx │ │ └── registration │ │ │ └── index.jsx │ ├── constants │ │ └── index.js │ ├── hooks │ │ └── profile │ │ │ ├── useFetchPosts.ts │ │ │ └── useFetchProfileDetails.ts │ ├── index.css │ ├── main.jsx │ └── pages │ │ ├── Campanies │ │ └── CampaniesPage.jsx │ │ ├── Home │ │ └── HomePage.tsx │ │ ├── Jobs │ │ └── JobsPage.jsx │ │ ├── Messages │ │ └── MessagesPage.jsx │ │ ├── Notifications │ │ └── NotificationsPage.jsx │ │ ├── Signup │ │ └── index.jsx │ │ ├── auth │ │ └── index.jsx │ │ ├── profile │ │ └── ProfilesPage.tsx │ │ └── projects │ │ └── ProjectsPage.jsx ├── tailwind.config.js ├── utils │ ├── credentialsencoder.ts │ ├── dateFormaters.ts │ └── types │ │ ├── index.ts │ │ ├── types.d.ts │ │ └── types.ts ├── vite-env.d.ts ├── vite.config.js └── vite.config.js.timestamp-1711703696331-3ba7dbf6070f1.mjs ├── package-lock.json └── package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .idea/ 3 | .vscode/ 4 | .gitignore 5 | .venv/ 6 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203 4 | ignore = D203, F722 5 | exclude = 6 | frontend/, 7 | .git, 8 | __pycache__, 9 | docs/source/conf.py, 10 | old, 11 | build, 12 | dist, 13 | venv, 14 | .venv, 15 | .env, 16 | migrations, 17 | addons, 18 | addons-dev, 19 | settings.py, 20 | max-complexity = 10 21 | -------------------------------------------------------------------------------- /.github/workflows/backend_lint.yml: -------------------------------------------------------------------------------- 1 | name: Backend_Pylint_Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - develop 8 | paths: 9 | - 'backend/**' 10 | push: 11 | branches: 12 | - main 13 | - develop 14 | paths: 15 | - 'backend/**' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | python-version: ["3.8", "3.9", "3.10"] 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install flake8 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install flake8 33 | working-directory: backend 34 | - name: Running flake8 35 | run: | 36 | flake8 . 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Application 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout Repository 11 | uses: actions/checkout@v3 12 | 13 | - name: Setup SSH Keys 14 | run: | 15 | mkdir -p ~/.ssh 16 | echo "${{ secrets.VPS_SSH_KEY }}" > ~/.ssh/id_rsa 17 | chmod 600 ~/.ssh/id_rsa 18 | ssh-keyscan -t rsa ${{ secrets.VPS_IP }} >> ~/.ssh/known_hosts 19 | 20 | - name: Deploy using Ansible 21 | run: ansible-playbook -i ansible/hosts --private-key ~/.ssh/id_rsa -u optimus ansible/deploy.yaml -------------------------------------------------------------------------------- /.github/workflows/front_end_eslint.yml: -------------------------------------------------------------------------------- 1 | name: Front-end Eslint 2 | 3 | on: 4 | push: 5 | branches: [ "main", "develop" ] 6 | paths: 7 | - 'frontend/**' 8 | pull_request: 9 | branches: [ "main", "develop" ] 10 | paths: 11 | - 'frontend/**' 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | node-version: [18.x] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'npm' 30 | cache-dependency-path: frontend/package-lock.json 31 | 32 | - name: Update npm 33 | working-directory: frontend 34 | run: npm install -g npm@latest 35 | - name: Install dependencies 36 | working-directory: frontend 37 | run : npm install 38 | - name: Lint code 39 | working-directory: frontend 40 | run: npm run lint 41 | 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | 162 | # VSCode 163 | .vscode/ 164 | 165 | 166 | # Logs 167 | logs 168 | *.log 169 | npm-debug.log* 170 | yarn-debug.log* 171 | yarn-error.log* 172 | pnpm-debug.log* 173 | lerna-debug.log* 174 | 175 | node_modules 176 | dist 177 | dist-ssr 178 | *.local 179 | 180 | # Editor directories and files 181 | .vscode/* 182 | !.vscode/extensions.json 183 | .idea 184 | .DS_Store 185 | *.suo 186 | *.ntvs* 187 | *.njsproj 188 | *.sln 189 | *.sw? 190 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # Image of workspace. Learn more: https://www.gitpod.io/docs/configure/workspaces/workspace-image 2 | image: gitpod/workspace-full:latest 3 | 4 | # List the start up tasks. Learn more: https://www.gitpod.io/docs/configure/workspaces/tasks 5 | tasks: 6 | - name: Setup Backend 7 | before: cd backend 8 | command: python -m venv venv && source venv/bin/activate && pip install -r requirements.txt && python manage.py makemigrations && python manage.py migrate && python manage.py runserver 9 | - name: Create Superuser Account 10 | before: cd backend 11 | command: python manage.py createsuperuser 12 | - name: Setup Frontend 13 | before: cd frontend 14 | command: npm install && npm run dev 15 | 16 | # List the ports to expose. Learn more: https://www.gitpod.io/docs/configure/workspaces/ports 17 | ports: 18 | - name: Frontend 19 | description: Port 5173 for the frontend 20 | port: 5173 21 | onOpen: open-browser 22 | - name: Open Backend 23 | description: Port 8000 for the frontend 24 | port: 8000 25 | onOpen: open-preview 26 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ChitChat 2 | 3 | First off, thanks for taking the time to contribute! ❤️ 4 | 5 | All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 6 | 7 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 8 | > - Star the project 9 | > - Tweet about it 10 | > - Refer this project in your project's readme 11 | > - Mention the project at local meetups and tell your friends/colleagues 12 | 13 | 14 | ## Table of Contents 15 | 16 | - [Code of Conduct](#code-of-conduct) 17 | - [I Have a Question](#i-have-a-question) 18 | - [I Want To Contribute](#i-want-to-contribute) 19 | - [Reporting Bugs](#reporting-bugs) 20 | - [Suggesting Enhancements](#suggesting-enhancements) 21 | - [Your First Code Contribution](#your-first-code-contribution) 22 | - [Improving The Documentation](#improving-the-documentation) 23 | - [Styleguides](#styleguides) 24 | - [Commit Messages](#commit-messages) 25 | - [Join The Project Team](#join-the-project-team) 26 | 27 | 28 | ## Code of Conduct 29 | 30 | This project and everyone participating in it is governed by the 31 | [ChitChat Code of Conduct](blob/master/CODE_OF_CONDUCT.md). 32 | By participating, you are expected to uphold this code. Please report unacceptable behavior 33 | to <>. 34 | 35 | 36 | ## I Have a Question 37 | 38 | > If you want to ask a question, we assume that you have read the available [Documentation](). 39 | 40 | Before you ask a question, it is best to search for existing [Issues](/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. 41 | 42 | If you then still feel the need to ask a question and need clarification, we recommend the following: 43 | 44 | - Open an [Issue](/issues/new). 45 | - Provide as much context as you can about what you're running into. 46 | - Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. 47 | 48 | We will then take care of the issue as soon as possible. 49 | 50 | 51 | 52 | ## I Want To Contribute 53 | 54 | > ### Legal Notice 55 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. 56 | 57 | ### Reporting Bugs 58 | 59 | 60 | #### Before Submitting a Bug Report 61 | 62 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. 63 | 64 | - Make sure that you are using the latest version. 65 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](). If you are looking for support, you might want to check [this section](#i-have-a-question)). 66 | - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](issues?q=label%3Abug). 67 | - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. 68 | - Collect information about the bug: 69 | - Stack trace (Traceback) 70 | - OS, Platform and Version (Windows, Linux, macOS, x86, ARM) 71 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant. 72 | - Possibly your input and the output 73 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 74 | 75 | 76 | #### How Do I Submit a Good Bug Report? 77 | 78 | > You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <>. 79 | 80 | 81 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 82 | 83 | - Open an [Issue](/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) 84 | - Explain the behavior you would expect and the actual behavior. 85 | - Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. 86 | - Provide the information you collected in the previous section. 87 | 88 | Once it's filed: 89 | 90 | - The project team will label the issue accordingly. 91 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. 92 | - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). 93 | 94 | 95 | 96 | 97 | ### Suggesting Enhancements 98 | 99 | This section guides you through submitting an enhancement suggestion for ChitChat, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. 100 | 101 | 102 | #### Before Submitting an Enhancement 103 | 104 | - Make sure that you are using the latest version. 105 | - Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration. 106 | - Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. 107 | - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. 108 | 109 | 110 | #### How Do I Submit a Good Enhancement Suggestion? 111 | 112 | Enhancement suggestions are tracked as [GitHub issues](/issues). 113 | 114 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 115 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 116 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 117 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. 118 | - **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 119 | 120 | 121 | 122 | ### Your First Code Contribution 123 | 124 | 125 | ### Improving The Documentation 126 | 127 | 128 | ## Styleguides 129 | ### Commit Messages 130 | 131 | 132 | ## Join The Project Team 133 | 134 | 135 | 136 | ## Attribution 137 | This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)! 138 | -------------------------------------------------------------------------------- /Contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | ## This project is a result of the hard work of the following volunteers 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 PythonBulawayo 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Documentation 2 | 3 | [![Open in GitPod](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/PythonBulawayo/ChitChat) 4 | 5 | ChitChat is a Python powered social media platform initiated by Buluwayo Python developers. 6 | 7 | The project is built using [Django](https://www.djangoproject.com/) for the backend and [React JS](https://react.dev/) for the frontend. The backend is located in the `backend` folder while the frontend is in the `frontend` folder. 8 | 9 | 10 | ## Installation 11 | 12 | Here's how to get yourself started with ChitChat on your machine. 13 | 14 | ### Backend Initialization 15 | 16 | Move into the backend folder from your terminal with the following command 17 | 18 | ```sh 19 | cd backend 20 | ``` 21 | 22 | Initialize the virtual environment 23 | 24 | ```sh 25 | python -m venv venv 26 | ``` 27 | 28 | Activate the virtual environment 29 | 30 | ```sh 31 | # Unix 32 | source venv/bin/activate 33 | 34 | # Windows 35 | venv\Scripts\activate 36 | ``` 37 | 38 | Install dependencies from the **requirements.txt** text file 39 | 40 | ```sh 41 | pip install -r requirements.txt 42 | ``` 43 | 44 | ### Backend Server Startup 45 | 46 | To start your server, run the following commands from the **backend** folder, on your terminal. 47 | 48 | Create database migrations from codebase 49 | 50 | ```sh 51 | python manage.py makemigrations 52 | ``` 53 | 54 | Install the database migration 55 | 56 | ```sh 57 | python manage.py migrate 58 | ``` 59 | 60 | Create a project super user 61 | 62 | ```sh 63 | python manage.py createsuperuser 64 | ``` 65 | 66 | Run the server 67 | 68 | ```sh 69 | python manage.py runserver 70 | ``` 71 | 72 | 73 | ### Frontend Initialization 74 | 75 | Move into the **frontend** folder 76 | 77 | ```sh 78 | cd frontend 79 | ``` 80 | 81 | Install npm modules 82 | 83 | ```sh 84 | npm install 85 | ``` 86 | 87 | Run the development server 88 | 89 | ```sh 90 | npm run dev 91 | ``` 92 | 93 | ### Setting Up Docker (Optional) 94 | 95 | #### Install Docker on Windows 96 | 97 | Download Docker Desktop from the official Docker website: [Docker Desktop for Windows](https://docs.docker.com/desktop/install/windows-install/) 98 | 99 | Follow the on-screen instructions to complete the installation. 100 | 101 | Once the installation is complete, Docker Desktop will be running on your system. 102 | 103 | #### Install Docker on MacOS 104 | 105 | Download Docker Desktop from the official Docker website: [Docker Desktop for Mac](https://docs.docker.com/desktop/install/mac-install/) 106 | 107 | Double-click the installer package to mount the Docker disk image. 108 | 109 | Drag the Docker icon to the Applications folder. 110 | 111 | Open Docker from the Applications folder and follow the on-screen instructions to complete the installation. 112 | 113 | Once the installation is complete, Docker Desktop will be running on your system. 114 | 115 | #### Install Docker on Linux 116 | 117 | NB: Refer to the official Docker documentation for updates: [Docker Desktop for Linux (Ubuntu)](https://docs.docker.com/engine/install/ubuntu/) 118 | 119 | Open a terminal window. 120 | 121 | Update the package index: 122 | 123 | ```sh 124 | sudo apt update 125 | ``` 126 | 127 | Install Docker dependencies: 128 | 129 | ```sh 130 | sudo apt install apt-transport-https ca-certificates curl software-properties-common 131 | ``` 132 | 133 | Add the Docker GPG key: 134 | 135 | ```sh 136 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 137 | ``` 138 | 139 | Set up the stable Docker repository: 140 | 141 | ```sh 142 | echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 143 | ``` 144 | 145 | Update the package index again: 146 | 147 | ```sh 148 | sudo apt update 149 | ``` 150 | 151 | Install Docker: 152 | 153 | ```sh 154 | sudo apt install docker-ce docker-ce-cli containerd.io 155 | ``` 156 | 157 | Docker should now be installed on your Linux system. Check if Docker is installed correctly: 158 | 159 | ```sh 160 | docker --version 161 | ``` 162 | 163 | #### Run the application 164 | 165 | To run this project using docker, please follow these steps: 166 | 167 | Change to the repository directory on your computer (if you are not already there): 168 | 169 | ```sh 170 | cd chitchat 171 | ``` 172 | 173 | In this directory you should see a `docker-compose.yaml` file 174 | 175 | Then in your terminal, run the following command: 176 | 177 | ```sh 178 | docker compose up --build 179 | ``` 180 | 181 | This should bring up the entire project. Good luck! 182 | 183 | ## Contributing 184 | 185 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. Please make sure to update tests as appropriate. This section is a simplified guide to make contributions. Follow the steps below to contribute: 186 | 187 | ### Fork the repository 188 | 189 | Fork this repository by clicking on the fork button on the top of this page. This will create a copy of this repository in your account. 190 | 191 | ![Fork Screenshot](./documentation/images/fork-screenshot.png) 192 | 193 | ### Clone the repository 194 | 195 | The next step is to clone the forked repository to your machine. Go to your GitHub account, open the forked repository, click on the code button and then click the copy to clipboard icon. The copied url should look like this: https://github.com/PythonBulawayo/ChitChat.git 196 | 197 | ![Clone screenshot](./documentation/images/clone-screenshot.png) 198 | 199 | ![Clipboard screenshot](./documentation/images/clipboard-screenshot.png) 200 | 201 | Open a terminal and run the following git command: 202 | ```sh 203 | git clone https://github.com/PythonBulawayo/ChitChat.git 204 | ``` 205 | 206 | If you prefer using SSH for Git operations, you can clone the repository using the SSH URL. 207 | 208 | Open a terminal and run the following git command: 209 | ```sh 210 | git clone git@github.com:PythonBulawayo/ChitChat.git 211 | ``` 212 | 213 | ### Create a branch 214 | 215 | Change to the repository directory on your computer (if you are not already there):
216 | ```sh 217 | cd chitchat 218 | ``` 219 | 220 | Now create a branch using the git switch command: 221 | ```sh 222 | git switch -c your-new-branch-name 223 | ``` 224 | 225 | For example: 226 | ```sh 227 | git switch -c update-documentation 228 | ``` 229 | 230 | ### Commit your changes 231 | 232 | Make necessary changes. 233 | 234 | To see the changes you have made, use the git status command: 235 | 236 | ```sh 237 | git status 238 | ``` 239 | 240 | Add those changes to the branch you just created using the git add command: 241 | 242 | ```sh 243 | git add the-file-you-made-changes-to 244 | ``` 245 | 246 | For example: 247 | 248 | ```sh 249 | git add read.md 250 | ``` 251 | 252 | Commit the changes using the git commit command: 253 | 254 | ```sh 255 | git commit -m "Update README with new information" 256 | ``` 257 | 258 | ### Push changes to GitHub 259 | 260 | Push your changes using the git push command: 261 | 262 | ```sh 263 | git push -u origin your-branch-name 264 | ``` 265 | 266 | Replace your-branch-name with the name of the branch you created earlier. 267 | 268 | For example: 269 | ```sh 270 | git push -u origin update-documentation 271 | ``` 272 | 273 | ### Submit your changes for review 274 | 275 | If you go to your repository on GitHub, you'll see a Compare & pull request button. Click on that button. 276 | 277 | 1. Create a pull request 278 | 279 | 2. Now submit the pull request. 280 | 281 | ### What next? 282 | 283 | **Reviewing your Changes:** 284 | 285 | Your changes will thoroughly be reviewed by the collaborator and other contributors in your pull request to ensure that the changes align with the purpose of the pull request. 286 | 287 | **Merging the Pull Request:** 288 | 289 | Once your pull request has been reviewed and all checks pass, it will be merged into the target branch. 290 | 291 | ## License 292 | 293 | Licensed under [MIT](./LICENSE.md) 294 | -------------------------------------------------------------------------------- /ansible/README.md: -------------------------------------------------------------------------------- 1 | # Ansible playbooks for server setup and deployment 2 | 3 | ## Prerequisites 4 | 5 | The setup script assumes you have an SSH public key called `id_ed25519` in your `~/.ssh/` directory. Create one before 6 | running it: 7 | 8 | Create an SSH keypair in your `~/.ssh/` directory: 9 | 10 | ```shell 11 | ssh-keygen -t ed25519 12 | 13 | ``` 14 | 15 | 16 | ## Setup the server: 17 | 18 | ```sh 19 | ansible-playbook setup.yml --verbose 20 | ``` 21 | 22 | 23 | Deploy: 24 | ```sh 25 | ansible-playbook deploy.yml --verbose 26 | ``` -------------------------------------------------------------------------------- /ansible/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = ./hosts -------------------------------------------------------------------------------- /ansible/deploy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: cybertron 4 | gather_facts: true 5 | become_user: "{{ ansible_user }}" 6 | 7 | tasks: 8 | 9 | - name: Checkout Repository 10 | git: 11 | repo: git@github.com:{{ repo_name }}.git 12 | accept_hostkey: true 13 | dest: "{{ repo_folder }}" 14 | version: "{{ repo_branch }}" 15 | 16 | key_file: /home/{{ ansible_user }}/.ssh/id_ed25519 17 | 18 | - name: Build and Start Project 19 | command: 20 | docker compose up --build --detach 21 | args: 22 | chdir: "{{ repo_folder }}" 23 | register: build_output 24 | 25 | - debug: 26 | var: build_output -------------------------------------------------------------------------------- /ansible/hosts: -------------------------------------------------------------------------------- 1 | [cybertron] 2 | 54.220.70.177 3 | 4 | [cybertron:vars] 5 | ansible_user=optimus 6 | repo_folder="/home/{{ansible_user}}/chitchat" 7 | repo_name=PythonBulawayo/ChitChat 8 | repo_branch=deploy 9 | -------------------------------------------------------------------------------- /ansible/roles/common/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install npm 3 | apt: 4 | name: 5 | - npm 6 | state: present 7 | -------------------------------------------------------------------------------- /ansible/roles/docker/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Add Docker GPG key 4 | apt_key: 5 | url: https://download.docker.com/linux/ubuntu/gpg 6 | state: present 7 | 8 | - name: Add Docker Repository 9 | apt_repository: 10 | repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable 11 | state: present 12 | 13 | - name: Install Docker 14 | apt: 15 | name: 16 | - docker-ce 17 | - docker-ce-cli 18 | - containerd.io 19 | - docker-buildx-plugin 20 | - docker-compose-plugin -------------------------------------------------------------------------------- /ansible/roles/security/tasks/main.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Prerequisites 3 | ansible.builtin.apt: 4 | name: aptitude 5 | update_cache: yes 6 | state: latest 7 | 8 | - name: Upgrade apt to the latest packages 9 | apt: 10 | upgrade: safe 11 | 12 | - name: Install required system packages 13 | apt: 14 | name: "{{ sys_packages}}" 15 | state: latest 16 | 17 | - name: Make sure we have a 'prime' group 18 | group: 19 | name: prime 20 | state: present 21 | 22 | - name: Allow 'prime' group to have passwordless sudo 23 | lineinfile: 24 | path: /etc/sudoers 25 | state: present 26 | regexp: "^%prime" 27 | line: "%prime ALL=(ALL) NOPASSWD: ALL" 28 | validate: "/usr/sbin/visudo -cf %s" 29 | 30 | - name: Create regular user with sudo priviledges 31 | user: 32 | name: "{{ansible_user}}" 33 | state: present 34 | groups: prime 35 | append: true 36 | create_home: true 37 | shell: /bin/bash 38 | 39 | - name: Set authorised key for remote user 40 | authorized_key: 41 | user: "{{ ansible_user}}" 42 | state: present 43 | key: "{{copy_local_ssh_key}}" 44 | 45 | - name: Enable PubKey Authentication 46 | lineinfile: 47 | path: /etc/ssh/sshd_config 48 | state: present 49 | regexp: "^#?PubkeyAuthentication" 50 | line: "PubkeyAuthentication yes" 51 | 52 | - name: Disallow password authentication 53 | lineinfile: 54 | path: /etc/ssh/sshd_config 55 | state: present 56 | regexp: "^#?PasswordAuthentication" 57 | line: "PasswordAuthentication no" 58 | register: disallow_pw 59 | 60 | - name: Disallow Root SSH access 61 | lineinfile: 62 | path: /etc/ssh/sshd_config 63 | state: present 64 | regexp: "^#?PermitRootLogin" 65 | line: "PermitRootLogin no" 66 | register: disallow_root_ssh 67 | 68 | - name: Restart sshd 69 | service: 70 | name: sshd 71 | state: restarted 72 | when: disallow_pw.changed or disallow_root_ssh.changed 73 | 74 | - name: UFW - Allow SSH connections 75 | ufw: 76 | rule: allow 77 | port: "{{ssh_port}}" 78 | proto: tcp 79 | 80 | - name: UFW - Deny all other incoming traffic by default 81 | ufw: 82 | state: enabled 83 | policy: deny 84 | direction: incoming 85 | -------------------------------------------------------------------------------- /ansible/setup.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: ChitChat Server Initial Setup 3 | hosts: aardwolf 4 | become: true 5 | vars: 6 | ansible_user: optimus 7 | ansible_port: "{{ssh_port}}" 8 | sys_packages: 9 | [ 10 | "curl", 11 | "git", 12 | "gnupg", 13 | "ca-certificates", 14 | "apt-transport-https", 15 | "software-properties-common", 16 | "ufw", 17 | ] 18 | copy_local_ssh_key: "{{lookup('ansible.builtin.file', lookup('ansible.builtin.env', 'HOME') + '/.ssh/id_ed25519.pub') }}" 19 | server_name: aardwolf 20 | roles: 21 | - role: security 22 | - role: docker 23 | - role: common 24 | 25 | 26 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-slim 2 | 3 | WORKDIR /app 4 | COPY requirements.txt /app 5 | RUN pip install -r requirements.txt 6 | COPY . /app 7 | 8 | # Perform migrations 9 | RUN python manage.py makemigrations 10 | RUN python manage.py migrate 11 | 12 | EXPOSE 8000 13 | CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] 14 | -------------------------------------------------------------------------------- /backend/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/accounts/__init__.py -------------------------------------------------------------------------------- /backend/accounts/admin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for managing admin functionality related to accounts. 3 | """ 4 | from django.contrib import admin 5 | from django.contrib.auth.admin import UserAdmin 6 | from django.contrib.auth.models import Group 7 | 8 | from .forms import CustomUserCreationForm, CustomUserChangeForm 9 | from .models import CustomUser, Profile 10 | 11 | 12 | class ProfileInline(admin.StackedInline): 13 | model = Profile 14 | 15 | 16 | class CustomUserAdmin(UserAdmin): 17 | add_form = CustomUserCreationForm 18 | form = CustomUserChangeForm 19 | model = CustomUser 20 | list_display = ["username", "email"] 21 | inlines = [ProfileInline] 22 | fieldsets = ((None, {"fields": ("username",)}),) 23 | 24 | 25 | admin.site.register(CustomUser, CustomUserAdmin) 26 | admin.site.unregister(Group) 27 | admin.site.register(Profile) 28 | -------------------------------------------------------------------------------- /backend/accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "accounts" 7 | 8 | def ready(self): 9 | import accounts.signals # noqa 10 | -------------------------------------------------------------------------------- /backend/accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.forms import UserCreationForm, UserChangeForm 2 | from .models import CustomUser 3 | 4 | 5 | class CustomUserCreationForm(UserCreationForm): 6 | class Meta: 7 | model = CustomUser 8 | fields = ("username", "email") 9 | 10 | 11 | class CustomUserChangeForm(UserChangeForm): 12 | class Meta: 13 | model = CustomUser 14 | fields = ("username", "email") 15 | -------------------------------------------------------------------------------- /backend/accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-30 12:22 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | from django.db import migrations, models 6 | import django.utils.timezone 7 | 8 | 9 | class Migration(migrations.Migration): 10 | initial = True 11 | 12 | dependencies = [ 13 | ("auth", "0012_alter_user_first_name_max_length"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="CustomUser", 19 | fields=[ 20 | ( 21 | "id", 22 | models.BigAutoField( 23 | auto_created=True, 24 | primary_key=True, 25 | serialize=False, 26 | verbose_name="ID", 27 | ), 28 | ), 29 | ("password", models.CharField(max_length=128, verbose_name="password")), 30 | ( 31 | "last_login", 32 | models.DateTimeField( 33 | blank=True, null=True, verbose_name="last login" 34 | ), 35 | ), 36 | ( 37 | "is_superuser", 38 | models.BooleanField( 39 | default=False, 40 | help_text="Designates that this user has all permissions without explicitly assigning them.", 41 | verbose_name="superuser status", 42 | ), 43 | ), 44 | ( 45 | "username", 46 | models.CharField( 47 | error_messages={ 48 | "unique": "A user with that username already exists." 49 | }, 50 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 51 | max_length=150, 52 | unique=True, 53 | validators=[ 54 | django.contrib.auth.validators.UnicodeUsernameValidator() 55 | ], 56 | verbose_name="username", 57 | ), 58 | ), 59 | ( 60 | "first_name", 61 | models.CharField( 62 | blank=True, max_length=150, verbose_name="first name" 63 | ), 64 | ), 65 | ( 66 | "last_name", 67 | models.CharField( 68 | blank=True, max_length=150, verbose_name="last name" 69 | ), 70 | ), 71 | ( 72 | "email", 73 | models.EmailField( 74 | blank=True, max_length=254, verbose_name="email address" 75 | ), 76 | ), 77 | ( 78 | "is_staff", 79 | models.BooleanField( 80 | default=False, 81 | help_text="Designates whether the user can log into this admin site.", 82 | verbose_name="staff status", 83 | ), 84 | ), 85 | ( 86 | "is_active", 87 | models.BooleanField( 88 | default=True, 89 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 90 | verbose_name="active", 91 | ), 92 | ), 93 | ( 94 | "date_joined", 95 | models.DateTimeField( 96 | default=django.utils.timezone.now, verbose_name="date joined" 97 | ), 98 | ), 99 | ( 100 | "groups", 101 | models.ManyToManyField( 102 | blank=True, 103 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 104 | related_name="user_set", 105 | related_query_name="user", 106 | to="auth.group", 107 | verbose_name="groups", 108 | ), 109 | ), 110 | ( 111 | "user_permissions", 112 | models.ManyToManyField( 113 | blank=True, 114 | help_text="Specific permissions for this user.", 115 | related_name="user_set", 116 | related_query_name="user", 117 | to="auth.permission", 118 | verbose_name="user permissions", 119 | ), 120 | ), 121 | ], 122 | options={ 123 | "verbose_name": "user", 124 | "verbose_name_plural": "users", 125 | "abstract": False, 126 | }, 127 | managers=[ 128 | ("objects", django.contrib.auth.models.UserManager()), 129 | ], 130 | ), 131 | ] 132 | -------------------------------------------------------------------------------- /backend/accounts/migrations/0002_profile.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2023-08-30 12:42 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | dependencies = [ 10 | ("accounts", "0001_initial"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="Profile", 16 | fields=[ 17 | ( 18 | "id", 19 | models.BigAutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ( 27 | "follows", 28 | models.ManyToManyField( 29 | blank=True, related_name="followed_by", to="accounts.profile" 30 | ), 31 | ), 32 | ( 33 | "user", 34 | models.OneToOneField( 35 | on_delete=django.db.models.deletion.CASCADE, 36 | to=settings.AUTH_USER_MODEL, 37 | ), 38 | ), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /backend/accounts/migrations/0003_alter_profile_follows.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2024-01-26 20:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("accounts", "0002_profile"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="profile", 14 | name="follows", 15 | field=models.ManyToManyField( 16 | blank=True, related_name="followers", to="accounts.profile" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/accounts/migrations/0004_alter_profile_options.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2024-02-08 14:49 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("accounts", "0003_alter_profile_follows"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterModelOptions( 13 | name="profile", 14 | options={"ordering": ["-pk"]}, 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /backend/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /backend/accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | 4 | 5 | class CustomUser(AbstractUser): 6 | pass 7 | 8 | def __str__(self): 9 | return self.username 10 | 11 | 12 | class Profile(models.Model): 13 | user = models.OneToOneField(CustomUser, on_delete=models.CASCADE) 14 | follows = models.ManyToManyField( 15 | "self", related_name="followers", symmetrical=False, blank=True 16 | ) 17 | 18 | def __str__(self): 19 | return f" {self.user.username} - Profile" 20 | 21 | class Meta: 22 | ordering = ["-pk"] 23 | -------------------------------------------------------------------------------- /backend/accounts/signals.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | from .models import CustomUser, Profile 3 | from django.dispatch import receiver 4 | 5 | 6 | @receiver(post_save, sender=CustomUser) 7 | def create_profile(sender, instance, created, **kwargs): 8 | if created: 9 | user_profile = Profile.objects.create(user=instance) 10 | user_profile.save() 11 | user_profile.follows.add(instance.profile) 12 | user_profile.save() 13 | -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/api/__init__.py -------------------------------------------------------------------------------- /backend/api/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/api/admin.py -------------------------------------------------------------------------------- /backend/api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ApiConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "api" 7 | -------------------------------------------------------------------------------- /backend/api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/api/migrations/__init__.py -------------------------------------------------------------------------------- /backend/api/serializers.py: -------------------------------------------------------------------------------- 1 | from accounts.models import Profile, CustomUser 2 | from core.models import Post 3 | from rest_framework import serializers 4 | 5 | 6 | class UserSerializer(serializers.ModelSerializer): 7 | class Meta: 8 | model = CustomUser 9 | # lookup_field = "pk" 10 | exclude = ["password", "groups", "user_permissions", "is_staff", "is_active"] 11 | 12 | 13 | class FollowerSerializer(serializers.ModelSerializer): 14 | url = serializers.HyperlinkedIdentityField( 15 | view_name="api:profile-detail", lookup_field="pk" 16 | ) 17 | username = serializers.SerializerMethodField() 18 | 19 | def get_username(self, obj): 20 | return f"{obj.user.username}" 21 | 22 | class Meta: 23 | model = Profile 24 | fields = ["username", "url"] 25 | 26 | 27 | class ProfileSerializer(serializers.ModelSerializer): 28 | url = serializers.HyperlinkedIdentityField( 29 | view_name="api:profile-detail", lookup_field="pk" 30 | ) 31 | user = serializers.SerializerMethodField() 32 | follows = FollowerSerializer(many=True, read_only=True) 33 | followers = FollowerSerializer(many=True, read_only=True) 34 | 35 | def get_user(self, obj): 36 | return obj.user.username 37 | 38 | class Meta: 39 | model = Profile 40 | fields = "__all__" 41 | lookup_field = "pk" 42 | 43 | 44 | class PostSerializer(serializers.ModelSerializer): 45 | url = serializers.HyperlinkedIdentityField( 46 | view_name="api:post-detail", lookup_field="pk" 47 | ) 48 | user_profile = serializers.HyperlinkedRelatedField( 49 | view_name="api:profile-detail", read_only=True 50 | ) 51 | 52 | class Meta: 53 | model = Post 54 | fields = "__all__" 55 | lookup_field = "pk" 56 | extra_kwargs = {"url": {"lookup_field": "pk"}} 57 | -------------------------------------------------------------------------------- /backend/api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from core.models import Post 3 | from accounts.models import CustomUser 4 | from django.urls import reverse 5 | from rest_framework import status 6 | 7 | 8 | class PostsTestCase(TestCase): 9 | def setUp(self): 10 | self.user = CustomUser.objects.create( 11 | username="test_user", 12 | email="test_user@example.com", 13 | password="test_password", 14 | ) 15 | self.post = Post.objects.create( 16 | user_profile=self.user.profile, body="This is a test post", likes_count=1 17 | ) 18 | self.relative_profile_url = reverse( 19 | "api:profile-detail", kwargs={"pk": self.post.user_profile.id} 20 | ) 21 | 22 | def test_post_creation(self): 23 | self.assertEqual(self.post.body, "This is a test post") 24 | self.assertEqual(self.post.user_profile, self.user.profile) 25 | self.assertEqual(self.post.likes_count, 1) 26 | 27 | def test_user_post_count(self): 28 | self.assertEqual(self.user.profile.posts.all().count(), 1) 29 | 30 | def test_post_detail_view(self): 31 | url = reverse("api:post-detail", kwargs={"pk": self.post.pk}) 32 | response = self.client.get(url) 33 | 34 | self.assertEqual(response.status_code, status.HTTP_200_OK) 35 | self.assertEqual(response.json()["likes_count"], 1) 36 | self.assertContains(response, self.relative_profile_url) 37 | self.assertContains(response, "This is a test post") 38 | 39 | def test_post_list_view(self): 40 | url = reverse("api:posts-list") 41 | response = self.client.get(url) 42 | 43 | self.assertEqual(response.status_code, status.HTTP_200_OK) 44 | self.assertEqual(len(response.json()), 1) 45 | 46 | def test_post_delete_view(self): 47 | url = reverse("api:post-delete", kwargs={"pk": self.post.pk}) 48 | response = self.client.delete(url) 49 | self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 50 | 51 | self.client.force_login(self.user) 52 | response = self.client.delete(url) 53 | self.assertEqual(response.status_code, 204) 54 | 55 | self.assertEqual(Post.objects.all().count(), 0) 56 | self.assertEqual(self.user.profile.posts.all().count(), 0) 57 | 58 | def test_post_create_view(self): 59 | url = reverse("api:post-create") 60 | response = self.client.post(url, {"body": "This is another post"}) 61 | self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) 62 | 63 | self.client.force_login(self.user) 64 | response = self.client.post(url, {"body": "This is another post"}) 65 | self.assertEqual(response.status_code, status.HTTP_201_CREATED) 66 | self.assertEqual(Post.objects.all().count(), 2) 67 | self.assertEqual(self.user.profile.posts.all().count(), 2) 68 | self.assertEqual(response.json()["body"], "This is another post") 69 | self.assertEqual(response.json()["likes_count"], 0) 70 | self.assertContains( 71 | response, self.relative_profile_url, status_code=status.HTTP_201_CREATED 72 | ) 73 | 74 | 75 | class APIRootTestCase(TestCase): 76 | def test_root_status_code(self): 77 | """ 78 | Test APIRootView for status code 200. 79 | """ 80 | url = reverse("api:api-root") 81 | response = self.client.get(url, HTTP_HOST="example.com") 82 | self.assertEqual(response.status_code, status.HTTP_200_OK) 83 | self.assertTrue(response.content) 84 | 85 | 86 | class UserTestCase(TestCase): 87 | def setUp(self): 88 | self.user = CustomUser.objects.create( 89 | username="test_user", 90 | email="test_user@example.com", 91 | password="test_password", 92 | ) 93 | 94 | self.client.force_login(self.user) 95 | 96 | def test_user_detail_view(self): 97 | """ 98 | Test User detail view for user information. 99 | """ 100 | url = reverse("api:user-detail", kwargs={"pk": self.user.pk}) 101 | response = self.client.get(url) 102 | self.assertEqual(response.status_code, status.HTTP_200_OK) 103 | self.assertEqual(url, f"/api/users/{self.user.pk}/") 104 | self.assertEqual(response.json()["username"], "test_user") 105 | self.assertEqual(response.json()["email"], "test_user@example.com") 106 | -------------------------------------------------------------------------------- /backend/api/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from api import views 3 | 4 | 5 | app_name = "api" 6 | urlpatterns = [ 7 | path("", views.APIRootView.as_view(), name="api-root"), 8 | path("api-auth/", include("rest_framework.urls", namespace="rest_framework")), 9 | path("api/users/", views.CustomUserList.as_view(), name="users"), 10 | path("api/users//", views.CustomUserDetail.as_view(), name="user-detail"), 11 | path("api/profiles/", views.ProfileList.as_view(), name="profiles"), 12 | path( 13 | "api/profiles//", views.ProfileDetail.as_view(), name="profile-detail" 14 | ), 15 | path( 16 | "api/follow//", views.FollowUserView.as_view(), name="follow-user" 17 | ), 18 | path( 19 | "api/unfollow//", 20 | views.UnFollowUserView.as_view(), 21 | name="unfollow-user", 22 | ), 23 | path("api/posts/", views.PostList.as_view(), name="posts-list"), 24 | path("api/posts/new/", views.PostCreate.as_view(), name="post-create"), 25 | path("api/posts//", views.PostDetail.as_view(), name="post-detail"), 26 | path("api/posts/delete//", views.PostDelete.as_view(), name="post-delete"), 27 | path("api/signup", views.SignUpView.as_view(), name="signup-view"), 28 | ] 29 | -------------------------------------------------------------------------------- /backend/api/views.py: -------------------------------------------------------------------------------- 1 | import django_filters.rest_framework 2 | from rest_framework import filters, generics, permissions, status 3 | from rest_framework.response import Response 4 | from rest_framework.views import APIView 5 | 6 | from accounts.models import CustomUser, Profile 7 | from core.models import Post 8 | 9 | from .serializers import PostSerializer, ProfileSerializer, UserSerializer 10 | 11 | User = CustomUser 12 | 13 | 14 | class APIRootView(APIView): 15 | """ 16 | The default Root view for the API 17 | """ 18 | 19 | def get(self, request): 20 | base_url = request.scheme + "://" + request.META["HTTP_HOST"] 21 | data = [ 22 | { 23 | "users": { 24 | "all": f"{base_url}/api/users/", 25 | }, 26 | "profiles": { 27 | "all": f"{base_url}/api/profiles/", 28 | }, 29 | "follow": {f"{base_url}/api/follow//"}, 30 | "unfollow": {f"{base_url}/api/unfollow//"}, 31 | "posts": {f"{base_url}/api/posts/"}, 32 | "post_detail": {f"{base_url}/api/posts//"}, 33 | "post_create": {f"{base_url}/api/posts/new/"}, 34 | "post_delete": {f"{base_url}/api/posts/delete//"}, 35 | } 36 | ] 37 | 38 | return Response(data) 39 | 40 | 41 | class CustomUserList(generics.ListAPIView): 42 | """ 43 | View to list all users 44 | 45 | """ 46 | 47 | queryset = User.objects.all() 48 | serializer_class = UserSerializer 49 | permission_classes = [permissions.IsAuthenticated] 50 | 51 | 52 | class CustomUserDetail(generics.RetrieveAPIView): 53 | queryset = User.objects.all() 54 | serializer_class = UserSerializer 55 | lookup_field = "pk" 56 | permission_classes = [permissions.IsAuthenticated] 57 | 58 | 59 | class ProfileList(generics.ListAPIView): 60 | """ 61 | View to list all profiles 62 | 63 | """ 64 | 65 | queryset = Profile.objects.all() 66 | serializer_class = ProfileSerializer 67 | 68 | 69 | class ProfileDetail(generics.RetrieveAPIView): 70 | """ 71 | View to view details of a profile 72 | 73 | """ 74 | 75 | queryset = Profile.objects.all() 76 | serializer_class = ProfileSerializer 77 | lookup_field = "pk" 78 | permission_classes = [permissions.IsAuthenticated] 79 | 80 | 81 | class FollowUserView(APIView): 82 | """ 83 | View to follow a user 84 | 85 | """ 86 | 87 | def get(self, request, username): 88 | profile = Profile.objects.get(user__username=username) 89 | return Response( 90 | {"message": f"you're about to follow {profile.user.username}"}, status=200 91 | ) 92 | 93 | def post(self, request, username): 94 | profile = Profile.objects.get(user__username=username) 95 | user = request.user 96 | profile.followers.add(user.profile) 97 | profile.save() 98 | return Response( 99 | {"message": f"you're now following {profile.user.username}"}, status=200 100 | ) 101 | 102 | 103 | class UnFollowUserView(APIView): 104 | """ 105 | View to unfollow a user 106 | 107 | """ 108 | 109 | def get(self, request, username): 110 | profile = Profile.objects.get(user__username=username) 111 | return Response( 112 | {"message": f"you're about to unfollow {profile.user.username}"}, status=200 113 | ) 114 | 115 | def post(self, request, username): 116 | profile = Profile.objects.get(user__username=username) 117 | user = request.user 118 | profile.followers.remove(user.profile) 119 | profile.save() 120 | return Response( 121 | {"message": f"you're no longer following {profile.user.username}"}, 122 | status=200, 123 | ) 124 | 125 | 126 | class SignUpView(APIView): 127 | """ 128 | View to register a new user 129 | 130 | Args: 131 | username: str 132 | email: str 133 | password: str 134 | """ 135 | 136 | def post(self, request): 137 | serializer = UserSerializer(data=request.data) 138 | if serializer.is_valid(): 139 | serializer.save() 140 | return Response(serializer.data, status=201) 141 | return Response(serializer.errors, status=400) 142 | 143 | 144 | class PostCreate(generics.CreateAPIView): 145 | """ 146 | View to create a new post 147 | """ 148 | 149 | queryset = Post.objects.all() 150 | serializer_class = PostSerializer 151 | permission_classes = [permissions.IsAuthenticated] 152 | 153 | def perform_create(self, serializer): 154 | serializer.save(user_profile=self.request.user.profile) 155 | 156 | 157 | class PostList(generics.ListAPIView): 158 | """ 159 | View to list all posts with filtering. 160 | """ 161 | 162 | queryset = Post.objects.all() 163 | serializer_class = PostSerializer 164 | filter_backends = [ 165 | filters.OrderingFilter, 166 | django_filters.rest_framework.DjangoFilterBackend, 167 | ] 168 | ordering_fields = ["created_at", "likes_count"] 169 | filterset_fields = ["user_profile", "created_at", "likes_count"] 170 | 171 | 172 | class PostDetail(generics.RetrieveAPIView): 173 | """ 174 | View to show a single post. 175 | """ 176 | 177 | queryset = Post.objects.all() 178 | serializer_class = PostSerializer 179 | lookup_field = "pk" 180 | 181 | 182 | class PostDelete(generics.DestroyAPIView): 183 | """ 184 | View to delete a single post. 185 | """ 186 | 187 | queryset = Post.objects.all() 188 | serializer_class = PostSerializer 189 | lookup_field = "pk" 190 | 191 | def delete(self, request, *args, **kwargs): 192 | post = self.get_object() 193 | if post.user_profile.user == request.user: 194 | return self.destroy(request, *args, **kwargs) 195 | else: 196 | return Response(status=status.HTTP_403_FORBIDDEN) 197 | -------------------------------------------------------------------------------- /backend/chitchat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/chitchat/__init__.py -------------------------------------------------------------------------------- /backend/chitchat/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for chitchat project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chitchat.settings") 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /backend/chitchat/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for chitchat project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "django-insecure-jtz*%h2*r7@bk0z^3^bnyr*5z71s-d5abtq5+%=iwf4rzx#bih" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ["*"] 29 | 30 | AUTH_USER_MODEL = "accounts.CustomUser" 31 | 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = [ 36 | # Django apps 37 | "django.contrib.admin", 38 | "django.contrib.auth", 39 | "django.contrib.contenttypes", 40 | "django.contrib.sessions", 41 | "django.contrib.messages", 42 | "django.contrib.staticfiles", 43 | # Third Party apps 44 | "rest_framework", 45 | "corsheaders", 46 | "django_filters", 47 | # Local apps 48 | "api", 49 | "accounts", 50 | "core", 51 | # Dev tools 52 | "django_extensions", 53 | "drf_yasg", 54 | ] 55 | 56 | MIDDLEWARE = [ 57 | "django.middleware.security.SecurityMiddleware", 58 | "corsheaders.middleware.CorsMiddleware", # CORS 59 | "django.contrib.sessions.middleware.SessionMiddleware", 60 | "django.middleware.common.CommonMiddleware", 61 | "django.middleware.csrf.CsrfViewMiddleware", 62 | "django.contrib.auth.middleware.AuthenticationMiddleware", 63 | "django.contrib.messages.middleware.MessageMiddleware", 64 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 65 | ] 66 | 67 | ROOT_URLCONF = "chitchat.urls" 68 | 69 | TEMPLATES = [ 70 | { 71 | "BACKEND": "django.template.backends.django.DjangoTemplates", 72 | "DIRS": [], 73 | "APP_DIRS": True, 74 | "OPTIONS": { 75 | "context_processors": [ 76 | "django.template.context_processors.debug", 77 | "django.template.context_processors.request", 78 | "django.contrib.auth.context_processors.auth", 79 | "django.contrib.messages.context_processors.messages", 80 | ], 81 | }, 82 | }, 83 | ] 84 | 85 | WSGI_APPLICATION = "chitchat.wsgi.application" 86 | 87 | 88 | # Database 89 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 90 | 91 | DATABASES = { 92 | "default": { 93 | "ENGINE": "django.db.backends.sqlite3", 94 | "NAME": BASE_DIR / "db.sqlite3", 95 | } 96 | } 97 | 98 | 99 | # Password validation 100 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 101 | 102 | AUTH_PASSWORD_VALIDATORS = [ 103 | { 104 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 105 | }, 106 | { 107 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 108 | }, 109 | { 110 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 111 | }, 112 | { 113 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 114 | }, 115 | ] 116 | 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 120 | 121 | LANGUAGE_CODE = "en-us" 122 | 123 | TIME_ZONE = "UTC" 124 | 125 | USE_I18N = True 126 | 127 | USE_TZ = True 128 | 129 | 130 | # Static files (CSS, JavaScript, Images) 131 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 132 | 133 | STATIC_URL = "static/" 134 | 135 | # Default primary key field type 136 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 137 | 138 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 139 | 140 | 141 | # Django Rest Framework Settings 142 | 143 | REST_FRAMEWORK = { 144 | # Use Django's standard `django.contrib.auth` permissions, 145 | # or allow read-only access for unauthenticated users. 146 | "DEFAULT_PERMISSION_CLASSES": [ 147 | # "rest_framework.permissions.IsAuthenticated", 148 | ], 149 | "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], 150 | } 151 | 152 | 153 | CORS_ALLOWED_ORIGINS = [ 154 | "http://localhost:5173", 155 | ] 156 | -------------------------------------------------------------------------------- /backend/chitchat/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for chitchat project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from rest_framework import permissions 20 | from drf_yasg.views import get_schema_view 21 | from drf_yasg import openapi 22 | 23 | schema_view = get_schema_view( 24 | openapi.Info( 25 | title="ChitChat API", 26 | default_version="v1", 27 | description="ChitChat API documentation.", 28 | ), 29 | public=True, 30 | permission_classes=(permissions.AllowAny,), 31 | ) 32 | 33 | urlpatterns = [ 34 | path("", include("api.urls")), 35 | path("admin/", admin.site.urls), 36 | path("api-auth/", include("rest_framework.urls")), 37 | # A JSON view of your API specification at /swagger.json 38 | # A YAML view of your API specification at /swagger.yaml 39 | # A swagger-ui view of your API specification at /swagger/ 40 | # A ReDoc view of your API specification at /redoc/ 41 | path( 42 | "swagger/", schema_view.without_ui(cache_timeout=0), name="schema-json" 43 | ), 44 | path( 45 | "swagger/", 46 | schema_view.with_ui("swagger", cache_timeout=0), 47 | name="schema-swagger-ui", 48 | ), 49 | path("redoc/", schema_view.with_ui("redoc", cache_timeout=0), name="schema-redoc"), 50 | ] 51 | -------------------------------------------------------------------------------- /backend/chitchat/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for chitchat project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chitchat.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/core/__init__.py -------------------------------------------------------------------------------- /backend/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Post 3 | 4 | admin.site.register(Post) 5 | -------------------------------------------------------------------------------- /backend/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "core" 7 | -------------------------------------------------------------------------------- /backend/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2024-02-08 14:49 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [ 11 | ("accounts", "0004_alter_profile_options"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="Post", 17 | fields=[ 18 | ( 19 | "id", 20 | models.BigAutoField( 21 | auto_created=True, 22 | primary_key=True, 23 | serialize=False, 24 | verbose_name="ID", 25 | ), 26 | ), 27 | ("body", models.CharField(max_length=280)), 28 | ("created_at", models.DateTimeField(auto_now_add=True)), 29 | ("likes_count", models.IntegerField(default=0)), 30 | ( 31 | "user", 32 | models.ForeignKey( 33 | on_delete=django.db.models.deletion.DO_NOTHING, 34 | related_name="posts", 35 | to="accounts.profile", 36 | ), 37 | ), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /backend/core/migrations/0002_rename_user_post_user_profile.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.4 on 2024-02-08 14:59 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("core", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="post", 14 | old_name="user", 15 | new_name="user_profile", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /backend/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/core/migrations/__init__.py -------------------------------------------------------------------------------- /backend/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from accounts.models import Profile 3 | 4 | 5 | class Post(models.Model): 6 | body = models.CharField(max_length=280) 7 | created_at = models.DateTimeField(auto_now_add=True) 8 | likes_count = models.IntegerField(default=0) 9 | user_profile = models.ForeignKey( 10 | Profile, related_name="posts", on_delete=models.DO_NOTHING 11 | ) 12 | 13 | def __str__(self): 14 | return f"{self.created_at.date()} - {self.user_profile.user} - {self.body}" 15 | -------------------------------------------------------------------------------- /backend/core/tests.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/core/tests.py -------------------------------------------------------------------------------- /backend/core/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/core/views.py -------------------------------------------------------------------------------- /backend/db.sqlite3.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/db.sqlite3.bak -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chitchat.settings") 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == "__main__": 22 | main() 23 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 88 3 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/backend/requirements.txt -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | build: 4 | context: ./backend 5 | ports: 6 | - "8000:8000" 7 | volumes: 8 | - ./backend:/app 9 | 10 | frontend: 11 | build: 12 | context: ./frontend 13 | expose: 14 | - 5173 15 | ports: 16 | - "5173:5173" 17 | volumes: 18 | - ./frontend:/app 19 | - /app/node_modules -------------------------------------------------------------------------------- /documentation/images/clipboard-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/documentation/images/clipboard-screenshot.png -------------------------------------------------------------------------------- /documentation/images/clone-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/documentation/images/clone-screenshot.png -------------------------------------------------------------------------------- /documentation/images/fork-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/documentation/images/fork-screenshot.png -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:prettier/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": "latest", 13 | "sourceType": "module" 14 | }, 15 | "plugins": ["react"], 16 | "rules": { 17 | "indent": ["error", "tab"], 18 | "linebreak-style": ["error", "unix"], 19 | "quotes": ["error", "double"], 20 | "semi": ["error", "always"], 21 | "prettier/prettier": ["error", { "endOfLine": "auto" }] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": false, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": true, 6 | "printWidth": 80, 7 | "useTabs": true, 8 | "jsxSingleQuote": false, 9 | "arrowParens": "always", 10 | "jsxBracketSameLine": false 11 | } 12 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-slim 2 | 3 | WORKDIR /app 4 | COPY package.json /app 5 | RUN npm install 6 | COPY . /app 7 | CMD [ "npm","run","dev", "--", "--host" ] -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | ChitChat 16 | 17 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "autoprefixer": "^10.4.16", 14 | "axios": "^1.5.0", 15 | "flowbite": "^1.8.1", 16 | "flowbite-react": "^0.6.0", 17 | "postcss": "^8.4.30", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-icons": "^5.3.0", 21 | "react-router-dom": "^6.16.0", 22 | "tailwindcss": "^3.3.3" 23 | }, 24 | "devDependencies": { 25 | "@types/react": "^18.2.15", 26 | "@types/react-dom": "^18.2.7", 27 | "@typescript-eslint/eslint-plugin": "^7.0.1", 28 | "@typescript-eslint/parser": "^7.0.1", 29 | "@vitejs/plugin-react": "^4.0.3", 30 | "daisyui": "^4.12.12", 31 | "eslint": "^8.56.0", 32 | "eslint-config-prettier": "^9.1.0", 33 | "eslint-plugin-prettier": "^5.1.3", 34 | "eslint-plugin-react": "^7.33.2", 35 | "eslint-plugin-react-hooks": "^4.6.0", 36 | "eslint-plugin-react-refresh": "^0.4.3", 37 | "prettier": "^3.2.5", 38 | "vite": "^4.4.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/public/user.png -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.jsx: -------------------------------------------------------------------------------- 1 | import Login from "./pages/auth"; 2 | import SignUpView from "./pages/Signup"; 3 | import { Routes, Route } from "react-router-dom"; 4 | import React from "react"; 5 | import { Home } from "./pages/Home/HomePage"; 6 | import { NavBar } from "./components/NavBar/NavBar"; 7 | import MessagesPage from "./pages/Messages/MessagesPage"; 8 | import ProfilesPage from "./pages/profile/ProfilesPage"; 9 | import NotificationsPage from "./pages/Notifications/NotificationsPage"; 10 | 11 | function App() { 12 | return ( 13 | <> 14 | 15 |
16 | 17 | } /> 18 | } />; 19 | } /> 20 | } /> 21 | } /> 22 | } /> 23 | 24 |
25 | 26 | ); 27 | } 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /frontend/src/assets/cc1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/cc1.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/cc2.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/cc2.ico -------------------------------------------------------------------------------- /frontend/src/assets/cc2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/cc2.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/cc3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/cc3.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/cc4 - Copy.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/cc4 - Copy.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/cc4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/cc4.jpeg -------------------------------------------------------------------------------- /frontend/src/assets/chitchat-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/chitchat-logo.png -------------------------------------------------------------------------------- /frontend/src/assets/profile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/profile.jpg -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PythonBulawayo/ChitChat/ea11090f810e3670588290f6f874c49d3b911879/frontend/src/assets/user.png -------------------------------------------------------------------------------- /frontend/src/components/Home/CreatePost.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CreatePost = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 | 12 | 19 |
20 |
21 | 27 |
28 | 43 |
44 |
45 |
46 |
47 |
48 | ); 49 | }; 50 | 51 | export default CreatePost; 52 | -------------------------------------------------------------------------------- /frontend/src/components/Home/MainCanvas.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CreatePost from "./CreatePost"; 3 | import Post from "../common/Post"; 4 | 5 | const MainCanvas = () => { 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default MainCanvas; 20 | -------------------------------------------------------------------------------- /frontend/src/components/Home/SideBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { images } from "../../constants"; 3 | import { Link } from "react-router-dom"; 4 | import useFetchProfileDetails from "../../hooks/profile/useFetchProfileDetails"; 5 | import SkeletonLoader from "../Profile/Loaders/HomeProfileSkeleton"; 6 | 7 | const SideBar = () => { 8 | const id = 6; 9 | const { isLoading, userProfileData, error, fetchProfileDetails } =useFetchProfileDetails(id) 10 | 11 | const userFirstName = userProfileData?.first_name; 12 | const userLastName = userProfileData?.last_name; 13 | const realname = `${userFirstName} ${userLastName}`; 14 | const userProfilePicture = userProfileData?.profile_picture; 15 | const numberOfFollowers = userProfileData?.followers.length; 16 | const numberOfFollowing = userProfileData?.follows.length; 17 | const username = userProfileData?.user; 18 | 19 | if(isLoading){ 20 | return 21 | } 22 | 23 | return ( 24 |
25 |
26 |
27 |
28 |

{username}

29 |
30 |
31 | profile 36 |
37 |
38 |
39 |

40 | Following 41 |

42 |

{numberOfFollowers}

43 |
44 |
45 |

46 | Followers 47 |

48 |

{numberOfFollowing}

49 |
50 | 58 |
59 | ); 60 | }; 61 | 62 | export default SideBar; 63 | -------------------------------------------------------------------------------- /frontend/src/components/Home/Suggested.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SlOptionsVertical } from "react-icons/sl"; 3 | 4 | import UserFollow from "../common/UserFollow"; 5 | 6 | const Suggested = () => { 7 | return ( 8 |
9 |
10 |

Suggested

11 | 18 |
19 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default Suggested; 27 | -------------------------------------------------------------------------------- /frontend/src/components/Home/TrendingTopics.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SlOptionsVertical } from "react-icons/sl"; 3 | import Trending from "../common/Trending"; 4 | 5 | const TrendingTopic = () => { 6 | return ( 7 |
8 |
9 |

Trending topics

10 | 17 |
18 | 19 |
20 | ); 21 | }; 22 | 23 | export default TrendingTopic; 24 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { images } from "../../constants"; 4 | import NavButtons from "./NavButtons"; 5 | import NavSearch from "./NavSearch"; 6 | 7 | export const NavBar = () => { 8 | const [dropdownOpen, setDropdownOpen] = useState(false); 9 | const [navOpen, setNavOpen] = useState(false); 10 | 11 | return ( 12 | 138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/NavButtons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const NavButtons = () => { 5 | return ( 6 |
    7 |
  • 8 | 13 | 28 | Home 29 | 30 |
  • 31 |
  • 32 | 37 | 52 | 53 | Messages 54 | 55 |
  • 56 |
  • 57 | 62 | 79 | 80 | Notifications 81 | 82 |
  • 83 |
84 | ); 85 | }; 86 | 87 | export default NavButtons; 88 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar/NavSearch.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NavSearch = () => { 4 | return ( 5 |
6 |
7 |
8 | 23 |
24 | 31 | 37 |
38 |
39 | ); 40 | }; 41 | 42 | export default NavSearch; 43 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/Followers.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useFetchProfileDetails from "../../hooks/profile/useFetchProfileDetails"; 3 | import FollowerSkeletonLoader from "./Loaders/FollowerSkeletonLoader"; 4 | import { useNavigate } from "react-router-dom"; 5 | import Button from "../common/Button"; 6 | import { FaUser } from "react-icons/fa"; 7 | 8 | interface FollowersProps { 9 | id: number; 10 | } 11 | 12 | const Followers: React.FC = ({ id }) => { 13 | const navigate = useNavigate(); 14 | 15 | const handleViewProfile = (url: string) => { 16 | const match = url.match(/\/profiles\/(\d+)\//); 17 | if (match) { 18 | const id = match[1]; 19 | navigate(`/profile/${id}`); 20 | } 21 | }; 22 | const { isLoading, userProfileData, error } = useFetchProfileDetails(id); 23 | 24 | if (isLoading) { 25 | return ; 26 | } 27 | 28 | if (error) { 29 | return
Error: {error}
; 30 | } 31 | 32 | 33 | return ( 34 |
35 |

Followers

36 |
    37 | {userProfileData?.followers?.map((follower) => ( 38 |
  • 42 | 43 | 44 |

    {follower.username}

    45 |
    46 | 49 |
  • 50 | )) ||

    No followers found.

    } 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default Followers; 57 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/Following.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useFetchProfileDetails from "../../hooks/profile/useFetchProfileDetails"; 3 | import FollowerSkeletonLoader from "./Loaders/FollowerSkeletonLoader"; 4 | import { useNavigate } from "react-router-dom"; 5 | import Button from "../common/Button"; 6 | import { FaUserCircle } from "react-icons/fa"; 7 | 8 | interface FollowingProps { 9 | id: number; 10 | } 11 | 12 | const Following: React.FC = ({ id }) => { 13 | const navigate = useNavigate(); 14 | const handleViewProfile = (url: string) => { 15 | const match = url.match(/\/profiles\/(\d+)\//); 16 | if (match) { 17 | const id = match[1]; 18 | navigate(`/profile/${id}`); 19 | } 20 | }; 21 | const { isLoading, userProfileData, error } = useFetchProfileDetails(id); 22 | 23 | if (isLoading) { 24 | return ; 25 | } 26 | 27 | if (error) { 28 | return
Error: {error}
; 29 | } 30 | 31 | return ( 32 |
33 |

Following

34 |
    35 | {userProfileData?.follows?.map((following) => ( 36 |
  • 40 | 41 | 42 |

    {following.username}

    43 |
    44 | 49 |
  • 50 | )) ||

    No following found.

    } 51 |
52 |
53 | ); 54 | }; 55 | 56 | export default Following; 57 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/Loaders/FollowerSkeletonLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const FollowerSkeletonLoader = () => { 4 | return ( 5 |
  • 6 |
    7 |
    8 |
  • 9 | ); 10 | }; 11 | 12 | export default FollowerSkeletonLoader; 13 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/Loaders/HomeProfileSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SkeletonLoader = () => { 4 | return ( 5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 | ); 28 | }; 29 | 30 | export default SkeletonLoader; 31 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/Loaders/PostsSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const PostsSkeletonLoader = () => { 4 | return ( 5 |
    6 |

    7 |
      8 | {Array.from({ length: 3 }).map((_, index) => ( 9 |
    • 13 |
      14 |
      15 |
      16 |
      17 |
      18 |
      19 |
      20 |
      21 |
      22 |
      23 |
      24 |
      25 |
      26 |
      27 |
      28 |
      29 |
    • 30 | ))} 31 |
    32 |
    33 | ); 34 | }; 35 | 36 | export default PostsSkeletonLoader; 37 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/Loaders/ProfileSkeletonLoader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const ProfileSkeletonLoader = () => { 4 | return ( 5 |
    6 |
    7 |
    8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 | ); 16 | } 17 | 18 | export default ProfileSkeletonLoader 19 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/Posts.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useFetchProfileDetails from "../../hooks/profile/useFetchProfileDetails"; 3 | import useFetchProfilePosts from "../../hooks/profile/useFetchPosts"; 4 | import PostsSkeletonLoader from "./Loaders/PostsSkeleton"; 5 | 6 | interface PostsProps { 7 | id: number; 8 | } 9 | 10 | const Posts: React.FC = ({ id }) => { 11 | const { 12 | userProfileData, 13 | isLoading: profileLoading, 14 | error: profileError, 15 | } = useFetchProfileDetails(id); 16 | const { 17 | posts, 18 | isLoading: postsLoading, 19 | error: postsError, 20 | } = useFetchProfilePosts(userProfileData?.url || ""); 21 | 22 | const profilePicture = userProfileData?.profile_picture; 23 | 24 | if (profileLoading || postsLoading) { 25 | return ; 26 | } 27 | 28 | if (profileError || postsError) { 29 | return
    Error: {profileError || postsError}
    ; 30 | } 31 | 32 | return ( 33 |
    34 |

    Posts

    35 |
      36 | {posts?.map((post) => ( 37 |
    • 41 |
      42 | avatar 50 |
      51 |
      52 |

      53 | {userProfileData?.user} 54 |

      55 |

      56 | {new Date(post.created_at).toLocaleString()} 57 |

      58 |
      59 |

      {post.body}

      60 |
      61 |
      62 | 68 | 74 | 75 | {post.likes_count} 76 |
      77 |
      78 | 84 | 90 | 91 | Comments 92 |
      93 |
      94 | 100 | 106 | 107 | Share 108 |
      109 |
      110 |
      111 |
      112 |
    • 113 | )) ||

      No posts available.

      } 114 |
    115 |
    116 | ); 117 | }; 118 | 119 | export default Posts; 120 | -------------------------------------------------------------------------------- /frontend/src/components/Profile/UserProfileCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useFetchProfileDetails from "../../hooks/profile/useFetchProfileDetails"; 3 | import { images } from "../../constants"; 4 | import ProfileSkeletonLoader from "./Loaders/ProfileSkeletonLoader"; 5 | import { FaUser, FaUserFriends } from "react-icons/fa"; 6 | 7 | interface UserProfileCardProps { 8 | id: number; 9 | } 10 | 11 | const UserProfileCard: React.FC = ({ id }) => { 12 | const { isLoading, userProfileData, error, fetchProfileDetails } = 13 | useFetchProfileDetails(id); 14 | 15 | const userFirstName = userProfileData?.first_name; 16 | const userLastName = userProfileData?.last_name; 17 | const realname = `${userFirstName} ${userLastName}`; 18 | const userProfilePicture = userProfileData?.profile_picture; 19 | const numberOfFollowers = userProfileData?.followers.length; 20 | const numberOfFollowing = userProfileData?.follows.length; 21 | const username = userProfileData?.user; 22 | 23 | if (isLoading) { 24 | return ; 25 | } 26 | return ( 27 |
    28 |
    29 | Profile 34 | 35 |
    36 |

    {username}

    37 |
    38 | 39 | 40 | 41 | 42 |

    43 | Followers {numberOfFollowers}{" "} 44 |

    45 |
    46 | 47 | 48 | 49 | 50 | 51 |

    52 | Following {numberOfFollowing}{" "} 53 |

    54 |
    55 |
    56 |
    57 |
    58 |
    59 | ); 60 | }; 61 | 62 | export default UserProfileCard; 63 | -------------------------------------------------------------------------------- /frontend/src/components/auth/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/react-in-jsx-scope */ 2 | import { Button, Card, TextInput, Avatar } from "flowbite-react"; 3 | import { useState } from "react"; 4 | import { useNavigate } from "react-router-dom"; 5 | 6 | export default function Login() { 7 | const [data, setData] = useState({ 8 | email: "", 9 | password: "", 10 | }); 11 | 12 | const navigate = useNavigate(); 13 | const handleNavigation = () => { 14 | navigate("/signup"); 15 | }; 16 | 17 | const onChange = (e) => { 18 | setData({ ...data, [e.target.id]: e.target.value }); 19 | }; 20 | return ( 21 | <> 22 |
    23 | 24 | 25 |

    26 | Welcome to Chit-Chat 27 |

    28 |
    29 |
    30 | 37 |
    38 |
    39 | 45 |
    46 | 47 | 50 | 53 |
    54 |
    55 |
    56 | 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Button = ({children, onClick}) => { 4 | return ( 5 | 12 | ); 13 | } 14 | 15 | export default Button 16 | -------------------------------------------------------------------------------- /frontend/src/components/common/Post.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { images } from "../../constants"; 3 | import { formatPostDate } from "../../../utils/dateFormaters"; 4 | import TechSTack from "./TechSTack"; 5 | import { AiOutlineLike } from "react-icons/ai"; 6 | import { GrView } from "react-icons/gr"; 7 | import { FaRegCommentAlt } from "react-icons/fa"; 8 | 9 | const techSTack = ["HTML", "React", "PHP", "Laravel"]; 10 | 11 | const Post = () => { 12 | const createdAt = "21-01-2024"; 13 | const numberoflIkes = 30; 14 | return ( 15 |
    16 |
    17 | user profile 22 |
    23 |

    Jessica William

    24 |

    {formatPostDate(createdAt)}

    25 |
    26 |
    27 |
    28 |

    29 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Dolorum, 30 | similique neque! Atque autem quibusdam, mollitia vero ea sequi nisi 31 | voluptatibus iste, provident corrupti eligendi cupiditate accusamus 32 | nesciunt, eum corporis harum. 33 |

    34 |
    35 |
    36 |
    37 | 41 | 45 | 49 |
    50 |
    51 | ); 52 | }; 53 | 54 | export default Post; 55 | -------------------------------------------------------------------------------- /frontend/src/components/common/Trending.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LuHash } from "react-icons/lu"; 3 | import { Link } from "react-router-dom"; 4 | 5 | const trending = ["Django", "Pycon Zimbabwe", "Flusk", "AWS"]; 6 | 7 | const Trending = () => { 8 | return ( 9 | <> 10 | {trending.map((item, i) => ( 11 |
    12 | 13 | 14 |

    {item}

    15 | 16 |
    17 | ))} 18 | 19 | ); 20 | }; 21 | 22 | export default Trending; 23 | -------------------------------------------------------------------------------- /frontend/src/components/common/UserFollow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { images } from "../../constants"; 3 | import { CiSquarePlus } from "react-icons/ci"; 4 | 5 | const UserFollow = () => { 6 | return ( 7 |
    8 |
    9 | user profile 14 |
    15 |

    Jessica William

    16 |

    Graphics designer

    17 |
    18 |
    19 | 26 |
    27 | ); 28 | }; 29 | 30 | export default UserFollow; 31 | -------------------------------------------------------------------------------- /frontend/src/components/registration/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/react-in-jsx-scope */ 2 | import { Button, Card, TextInput, Avatar } from "flowbite-react"; 3 | import { useState } from "react"; 4 | import axios from "axios"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | export default function Registration() { 8 | const [data, setData] = useState({ 9 | email: "", 10 | password: "", 11 | first_name: "", 12 | last_name: "", 13 | username: "", 14 | }); 15 | 16 | const navigate = useNavigate(); 17 | 18 | const onChange = (e) => { 19 | setData({ ...data, [e.target.id]: e.target.value }); 20 | }; 21 | 22 | const onSubmit = () => { 23 | if ( 24 | data.email === "" || 25 | data.password === "" || 26 | data.first_name === "" || 27 | data.last_name === "" || 28 | data.username === "" 29 | ) 30 | alert("All fields are required"); 31 | axios 32 | .post("http://localhost:8000/api/signup", data) 33 | .then(() => { 34 | alert("Success"); 35 | navigate("/users"); 36 | }) 37 | .catch(() => { 38 | alert("Failed to submit"); 39 | }); 40 | }; 41 | 42 | return ( 43 |
    44 | 45 | 46 |

    Welcome to Chit-Chat

    47 |
    48 |
    49 | 56 |
    57 |
    58 | 65 |
    66 |
    67 | 74 |
    75 |
    76 | 83 |
    84 |
    85 | 92 |
    93 | 94 | 95 |
    96 |
    97 |
    98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /frontend/src/constants/index.js: -------------------------------------------------------------------------------- 1 | import userProfile from "../assets/user.png"; 2 | import chitchatlogo from "../assets/chitchat-logo.png"; 3 | import profilePicture from "../assets/profile.jpg"; 4 | 5 | export const images = { 6 | userProfile, 7 | chitchatlogo, 8 | profilePicture, 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/src/hooks/profile/useFetchPosts.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { useEffect, useState } from "react"; 3 | import { Post } from "../../../utils/types"; 4 | 5 | const useFetchProfilePosts = (profileUrl: string) => { 6 | const [posts, setPosts] = useState([]); 7 | const [isLoading, setIsLoading] = useState(false); 8 | const [error, setError] = useState(null); 9 | 10 | useEffect(() => { 11 | const fetchPosts = async () => { 12 | setIsLoading(true); 13 | try { 14 | const response = await axios.get( 15 | `${import.meta.env.VITE_API_BASE_URL}/posts` 16 | ); 17 | // Filter posts where the user_profile matches the current profile 18 | const filteredPosts = response.data.filter( 19 | (post: Post) => post.user_profile === profileUrl 20 | ); 21 | setPosts(filteredPosts); 22 | } catch (error) { 23 | setError("Error fetching posts"); 24 | } finally { 25 | setIsLoading(false); 26 | } 27 | }; 28 | fetchPosts(); 29 | }, [profileUrl]); 30 | 31 | return { posts, isLoading, error }; 32 | }; 33 | 34 | export default useFetchProfilePosts; -------------------------------------------------------------------------------- /frontend/src/hooks/profile/useFetchProfileDetails.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useCallback } from "react"; 2 | import { UserProfile } from "../../../utils/types"; 3 | import axios from "axios"; 4 | import encodeCredentials from "../../../utils/credentialsencoder"; 5 | 6 | const useFetchProfileDetails = (id: number) => { 7 | const [userProfileData, setUserProfileData] = useState( 8 | null 9 | ); 10 | const [isLoading, setIsLoading] = useState(false); 11 | const [error, setError] = useState(null); 12 | const BASE_URL = import.meta.env.VITE_API_BASE_URL; 13 | const username = "chitchat-test"; 14 | const password = "7lBeZIE="; 15 | const credentials = encodeCredentials(username, password); 16 | 17 | const fetchProfileDetails = useCallback(async () => { 18 | setIsLoading(true); 19 | setError(null); 20 | try { 21 | const response = await axios.get( 22 | `${BASE_URL}/profiles/${id}`, 23 | { 24 | headers: { 25 | Authorization: `Basic ${credentials}`, 26 | }, 27 | } 28 | ); 29 | setUserProfileData(response?.data); 30 | 31 | } catch (error) { 32 | setError("Failed to fetch profile details. Try again later!"); 33 | } finally { 34 | setIsLoading(false); 35 | } 36 | }, [BASE_URL, id]); 37 | 38 | useEffect(() => { 39 | if (id) { 40 | fetchProfileDetails(); 41 | } 42 | }, [id, fetchProfileDetails]); 43 | 44 | return { isLoading, userProfileData, error, fetchProfileDetails }; 45 | }; 46 | 47 | export default useFetchProfileDetails; 48 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import "flowbite"; 6 | import { BrowserRouter as Router } from "react-router-dom"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root")).render( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /frontend/src/pages/Campanies/CampaniesPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Campanies = () => { 4 | return
    Campanies
    ; 5 | }; 6 | 7 | export default Campanies; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/Home/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import SideBar from "../../components/Home/SideBar"; 3 | import MainCanvas from "../../components/Home/MainCanvas"; 4 | import Suggested from "../../components/Home/Suggested"; 5 | import TrendingTopic from "../../components/Home/TrendingTopics"; 6 | 7 | export const Home = () => { 8 | return ( 9 |
    10 |
    11 |
    12 | 13 |
    14 |
    15 | 16 |
    17 |
    18 |
    19 | 20 | 21 |
    22 |
    23 |
    24 |
    25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /frontend/src/pages/Jobs/JobsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const JobsPage = () => { 4 | return
    JobsPage
    ; 5 | }; 6 | 7 | export default JobsPage; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/Messages/MessagesPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const MessagesPage = () => { 4 | return
    MessagesPage
    ; 5 | }; 6 | 7 | export default MessagesPage; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/Notifications/NotificationsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const NotificationsPage = () => { 4 | return
    NotificationsPage
    ; 5 | }; 6 | 7 | export default NotificationsPage; 8 | -------------------------------------------------------------------------------- /frontend/src/pages/Signup/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/react-in-jsx-scope */ 2 | import Registration from "../../components/registration"; 3 | 4 | export default function SignUpView() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/pages/auth/index.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/react-in-jsx-scope */ 2 | import Wrapper from "../../components/auth"; 3 | 4 | export default function Login() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/pages/profile/ProfilesPage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import UserProfileCard from "../../components/Profile/UserProfileCard"; 3 | import Followers from "../../components/Profile/Followers"; 4 | import Following from "../../components/Profile/Following"; 5 | import Posts from "../../components/Profile/Posts"; 6 | import { useParams } from "react-router-dom"; 7 | 8 | interface ProfilesPageProps { 9 | id: number; 10 | } 11 | 12 | const ProfilesPage: React.FC = () => { 13 | const { id } = useParams<{ id: string }>(); 14 | const numericId = Number(id); 15 | return ( 16 |
    17 | 18 | 19 | 20 | 21 |
    22 | ); 23 | }; 24 | 25 | export default ProfilesPage; 26 | -------------------------------------------------------------------------------- /frontend/src/pages/projects/ProjectsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Projects = () => { 4 | return
    Projects
    ; 5 | }; 6 | 7 | export default Projects; 8 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | /** @type {import('tailwindcss').Config} */ 3 | export default { 4 | content: [ 5 | "./src/**/*.{js,jsx,ts,tsx}", 6 | "node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}" /* src folder, for example */, 7 | ], 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: { 12 | 50: "#ffe5e5", 13 | 100: "#ffb3b3", 14 | 200: "#ff8080", 15 | 300: "#ff4d4d", 16 | 400: "#ff1a1a", 17 | 500: "#ff0000", // Base red 18 | }, 19 | subheading: { 20 | 100: "#5c5b5b ", 21 | }, 22 | }, 23 | typography: { 24 | fontFamily: { 25 | sans: ["Graphik", "sans-serif"], 26 | serif: ["Merriweather", "serif"], 27 | }, 28 | fontSize: { 29 | sm: ["14px", "20px"], 30 | base: ["16px", "24px"], 31 | lg: ["18px", "28px"], 32 | xl: ["20px", "32px"], 33 | }, 34 | fontWeight: { 35 | normal: 400, 36 | bold: 700, 37 | }, 38 | lineHeight: { 39 | normal: "normal", 40 | none: 1, 41 | tight: 1.25, 42 | snug: 1.375, 43 | relaxed: 1.625, 44 | loose: 2, 45 | }, 46 | letterSpacing: { 47 | tight: "-0.05em", 48 | normal: "0em", 49 | wide: "0.05em", 50 | }, 51 | }, 52 | }, 53 | }, 54 | plugins: [require("flowbite/plugin")], 55 | }; 56 | -------------------------------------------------------------------------------- /frontend/utils/credentialsencoder.ts: -------------------------------------------------------------------------------- 1 | 2 | const encodeCredentials = (username: string, password: string) => { 3 | return btoa(`${username}:${password}`); 4 | }; 5 | 6 | export default encodeCredentials 7 | -------------------------------------------------------------------------------- /frontend/utils/dateFormaters.ts: -------------------------------------------------------------------------------- 1 | export const formatPostDate = (createdAt: string | number | Date) => { 2 | const currentDate = new Date(); 3 | const createdAtDate = new Date(createdAt); 4 | 5 | const timeDifferenceInSeconds = Math.floor((currentDate.getTime() - createdAtDate.getTime()) / 1000); 6 | const timeDifferenceInMinutes = Math.floor(timeDifferenceInSeconds / 60); 7 | const timeDifferenceInHours = Math.floor(timeDifferenceInMinutes / 60); 8 | const timeDifferenceInDays = Math.floor(timeDifferenceInHours / 24); 9 | 10 | if (timeDifferenceInDays > 1) { 11 | return createdAtDate.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 12 | } else if (timeDifferenceInDays === 1) { 13 | return "1d"; 14 | } else if (timeDifferenceInHours >= 1) { 15 | return `${timeDifferenceInHours}h`; 16 | } else if (timeDifferenceInMinutes >= 1) { 17 | return `${timeDifferenceInMinutes}m`; 18 | } else { 19 | return "Just now"; 20 | } 21 | }; 22 | 23 | export const formatMemberSinceDate = (createdAt) => { 24 | const date = new Date(createdAt); 25 | const months = [ 26 | "January", 27 | "February", 28 | "March", 29 | "April", 30 | "May", 31 | "June", 32 | "July", 33 | "August", 34 | "September", 35 | "October", 36 | "November", 37 | "December", 38 | ]; 39 | const month = months[date.getMonth()]; 40 | const year = date.getFullYear(); 41 | return `Joined ${month} ${year}`; 42 | }; -------------------------------------------------------------------------------- /frontend/utils/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; -------------------------------------------------------------------------------- /frontend/utils/types/types.d.ts: -------------------------------------------------------------------------------- 1 | declare type TechStack = { 2 | title: string; 3 | }; 4 | -------------------------------------------------------------------------------- /frontend/utils/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface UserProfile { 2 | id: number; 3 | url: string; 4 | user: string; 5 | profile_picture: string; 6 | first_name: string; 7 | last_name: string; 8 | follows: Array<{ username: string; url: string }>; 9 | followers: Array<{username: string, url: string}>; 10 | } 11 | 12 | export interface ProfileList { 13 | profiles: UserProfile[]; 14 | } 15 | 16 | export interface Post { 17 | id: number; 18 | url: string; 19 | user_profile: string; 20 | body: string; 21 | created_at: string; 22 | likes_count: number; 23 | } 24 | 25 | export interface PostList { 26 | posts: Post[]; 27 | } -------------------------------------------------------------------------------- /frontend/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | server: { 7 | proxy: { 8 | "/api": { 9 | target: "http://chitchat-api.vndprojects.com", 10 | changeOrigin: true, 11 | rewrite: (path) => path.replace(/^\/api/, ""), 12 | }, 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /frontend/vite.config.js.timestamp-1711703696331-3ba7dbf6070f1.mjs: -------------------------------------------------------------------------------- 1 | // vite.config.js 2 | import { defineConfig } from "file:///C:/projects/ChitChat/frontend/node_modules/vite/dist/node/index.js"; 3 | import react from "file:///C:/projects/ChitChat/frontend/node_modules/@vitejs/plugin-react/dist/index.mjs"; 4 | var vite_config_default = defineConfig({ 5 | plugins: [react()], 6 | }); 7 | export { vite_config_default as default }; 8 | //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxwcm9qZWN0c1xcXFxDaGl0Q2hhdFxcXFxmcm9udGVuZFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiQzpcXFxccHJvamVjdHNcXFxcQ2hpdENoYXRcXFxcZnJvbnRlbmRcXFxcdml0ZS5jb25maWcuanNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L3Byb2plY3RzL0NoaXRDaGF0L2Zyb250ZW5kL3ZpdGUuY29uZmlnLmpzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSBcInZpdGVcIjtcbmltcG9ydCByZWFjdCBmcm9tIFwiQHZpdGVqcy9wbHVnaW4tcmVhY3RcIjtcblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcblx0cGx1Z2luczogW3JlYWN0KCldLFxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQWlSLFNBQVMsb0JBQW9CO0FBQzlTLE9BQU8sV0FBVztBQUVsQixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMzQixTQUFTLENBQUMsTUFBTSxDQUFDO0FBQ2xCLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== 9 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChitChat", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "vite": "^5.4.9" 9 | } 10 | }, 11 | "node_modules/@esbuild/aix-ppc64": { 12 | "version": "0.21.5", 13 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", 14 | "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", 15 | "cpu": [ 16 | "ppc64" 17 | ], 18 | "dev": true, 19 | "license": "MIT", 20 | "optional": true, 21 | "os": [ 22 | "aix" 23 | ], 24 | "engines": { 25 | "node": ">=12" 26 | } 27 | }, 28 | "node_modules/@esbuild/android-arm": { 29 | "version": "0.21.5", 30 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", 31 | "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", 32 | "cpu": [ 33 | "arm" 34 | ], 35 | "dev": true, 36 | "license": "MIT", 37 | "optional": true, 38 | "os": [ 39 | "android" 40 | ], 41 | "engines": { 42 | "node": ">=12" 43 | } 44 | }, 45 | "node_modules/@esbuild/android-arm64": { 46 | "version": "0.21.5", 47 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", 48 | "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", 49 | "cpu": [ 50 | "arm64" 51 | ], 52 | "dev": true, 53 | "license": "MIT", 54 | "optional": true, 55 | "os": [ 56 | "android" 57 | ], 58 | "engines": { 59 | "node": ">=12" 60 | } 61 | }, 62 | "node_modules/@esbuild/android-x64": { 63 | "version": "0.21.5", 64 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", 65 | "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", 66 | "cpu": [ 67 | "x64" 68 | ], 69 | "dev": true, 70 | "license": "MIT", 71 | "optional": true, 72 | "os": [ 73 | "android" 74 | ], 75 | "engines": { 76 | "node": ">=12" 77 | } 78 | }, 79 | "node_modules/@esbuild/darwin-arm64": { 80 | "version": "0.21.5", 81 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", 82 | "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", 83 | "cpu": [ 84 | "arm64" 85 | ], 86 | "dev": true, 87 | "license": "MIT", 88 | "optional": true, 89 | "os": [ 90 | "darwin" 91 | ], 92 | "engines": { 93 | "node": ">=12" 94 | } 95 | }, 96 | "node_modules/@esbuild/darwin-x64": { 97 | "version": "0.21.5", 98 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", 99 | "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", 100 | "cpu": [ 101 | "x64" 102 | ], 103 | "dev": true, 104 | "license": "MIT", 105 | "optional": true, 106 | "os": [ 107 | "darwin" 108 | ], 109 | "engines": { 110 | "node": ">=12" 111 | } 112 | }, 113 | "node_modules/@esbuild/freebsd-arm64": { 114 | "version": "0.21.5", 115 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", 116 | "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", 117 | "cpu": [ 118 | "arm64" 119 | ], 120 | "dev": true, 121 | "license": "MIT", 122 | "optional": true, 123 | "os": [ 124 | "freebsd" 125 | ], 126 | "engines": { 127 | "node": ">=12" 128 | } 129 | }, 130 | "node_modules/@esbuild/freebsd-x64": { 131 | "version": "0.21.5", 132 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", 133 | "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", 134 | "cpu": [ 135 | "x64" 136 | ], 137 | "dev": true, 138 | "license": "MIT", 139 | "optional": true, 140 | "os": [ 141 | "freebsd" 142 | ], 143 | "engines": { 144 | "node": ">=12" 145 | } 146 | }, 147 | "node_modules/@esbuild/linux-arm": { 148 | "version": "0.21.5", 149 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", 150 | "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", 151 | "cpu": [ 152 | "arm" 153 | ], 154 | "dev": true, 155 | "license": "MIT", 156 | "optional": true, 157 | "os": [ 158 | "linux" 159 | ], 160 | "engines": { 161 | "node": ">=12" 162 | } 163 | }, 164 | "node_modules/@esbuild/linux-arm64": { 165 | "version": "0.21.5", 166 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", 167 | "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", 168 | "cpu": [ 169 | "arm64" 170 | ], 171 | "dev": true, 172 | "license": "MIT", 173 | "optional": true, 174 | "os": [ 175 | "linux" 176 | ], 177 | "engines": { 178 | "node": ">=12" 179 | } 180 | }, 181 | "node_modules/@esbuild/linux-ia32": { 182 | "version": "0.21.5", 183 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", 184 | "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", 185 | "cpu": [ 186 | "ia32" 187 | ], 188 | "dev": true, 189 | "license": "MIT", 190 | "optional": true, 191 | "os": [ 192 | "linux" 193 | ], 194 | "engines": { 195 | "node": ">=12" 196 | } 197 | }, 198 | "node_modules/@esbuild/linux-loong64": { 199 | "version": "0.21.5", 200 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", 201 | "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", 202 | "cpu": [ 203 | "loong64" 204 | ], 205 | "dev": true, 206 | "license": "MIT", 207 | "optional": true, 208 | "os": [ 209 | "linux" 210 | ], 211 | "engines": { 212 | "node": ">=12" 213 | } 214 | }, 215 | "node_modules/@esbuild/linux-mips64el": { 216 | "version": "0.21.5", 217 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", 218 | "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", 219 | "cpu": [ 220 | "mips64el" 221 | ], 222 | "dev": true, 223 | "license": "MIT", 224 | "optional": true, 225 | "os": [ 226 | "linux" 227 | ], 228 | "engines": { 229 | "node": ">=12" 230 | } 231 | }, 232 | "node_modules/@esbuild/linux-ppc64": { 233 | "version": "0.21.5", 234 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", 235 | "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", 236 | "cpu": [ 237 | "ppc64" 238 | ], 239 | "dev": true, 240 | "license": "MIT", 241 | "optional": true, 242 | "os": [ 243 | "linux" 244 | ], 245 | "engines": { 246 | "node": ">=12" 247 | } 248 | }, 249 | "node_modules/@esbuild/linux-riscv64": { 250 | "version": "0.21.5", 251 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", 252 | "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", 253 | "cpu": [ 254 | "riscv64" 255 | ], 256 | "dev": true, 257 | "license": "MIT", 258 | "optional": true, 259 | "os": [ 260 | "linux" 261 | ], 262 | "engines": { 263 | "node": ">=12" 264 | } 265 | }, 266 | "node_modules/@esbuild/linux-s390x": { 267 | "version": "0.21.5", 268 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", 269 | "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", 270 | "cpu": [ 271 | "s390x" 272 | ], 273 | "dev": true, 274 | "license": "MIT", 275 | "optional": true, 276 | "os": [ 277 | "linux" 278 | ], 279 | "engines": { 280 | "node": ">=12" 281 | } 282 | }, 283 | "node_modules/@esbuild/linux-x64": { 284 | "version": "0.21.5", 285 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", 286 | "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", 287 | "cpu": [ 288 | "x64" 289 | ], 290 | "dev": true, 291 | "license": "MIT", 292 | "optional": true, 293 | "os": [ 294 | "linux" 295 | ], 296 | "engines": { 297 | "node": ">=12" 298 | } 299 | }, 300 | "node_modules/@esbuild/netbsd-x64": { 301 | "version": "0.21.5", 302 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", 303 | "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", 304 | "cpu": [ 305 | "x64" 306 | ], 307 | "dev": true, 308 | "license": "MIT", 309 | "optional": true, 310 | "os": [ 311 | "netbsd" 312 | ], 313 | "engines": { 314 | "node": ">=12" 315 | } 316 | }, 317 | "node_modules/@esbuild/openbsd-x64": { 318 | "version": "0.21.5", 319 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", 320 | "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", 321 | "cpu": [ 322 | "x64" 323 | ], 324 | "dev": true, 325 | "license": "MIT", 326 | "optional": true, 327 | "os": [ 328 | "openbsd" 329 | ], 330 | "engines": { 331 | "node": ">=12" 332 | } 333 | }, 334 | "node_modules/@esbuild/sunos-x64": { 335 | "version": "0.21.5", 336 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", 337 | "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", 338 | "cpu": [ 339 | "x64" 340 | ], 341 | "dev": true, 342 | "license": "MIT", 343 | "optional": true, 344 | "os": [ 345 | "sunos" 346 | ], 347 | "engines": { 348 | "node": ">=12" 349 | } 350 | }, 351 | "node_modules/@esbuild/win32-arm64": { 352 | "version": "0.21.5", 353 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", 354 | "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", 355 | "cpu": [ 356 | "arm64" 357 | ], 358 | "dev": true, 359 | "license": "MIT", 360 | "optional": true, 361 | "os": [ 362 | "win32" 363 | ], 364 | "engines": { 365 | "node": ">=12" 366 | } 367 | }, 368 | "node_modules/@esbuild/win32-ia32": { 369 | "version": "0.21.5", 370 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", 371 | "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", 372 | "cpu": [ 373 | "ia32" 374 | ], 375 | "dev": true, 376 | "license": "MIT", 377 | "optional": true, 378 | "os": [ 379 | "win32" 380 | ], 381 | "engines": { 382 | "node": ">=12" 383 | } 384 | }, 385 | "node_modules/@esbuild/win32-x64": { 386 | "version": "0.21.5", 387 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", 388 | "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", 389 | "cpu": [ 390 | "x64" 391 | ], 392 | "dev": true, 393 | "license": "MIT", 394 | "optional": true, 395 | "os": [ 396 | "win32" 397 | ], 398 | "engines": { 399 | "node": ">=12" 400 | } 401 | }, 402 | "node_modules/@rollup/rollup-android-arm-eabi": { 403 | "version": "4.24.0", 404 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", 405 | "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", 406 | "cpu": [ 407 | "arm" 408 | ], 409 | "dev": true, 410 | "license": "MIT", 411 | "optional": true, 412 | "os": [ 413 | "android" 414 | ] 415 | }, 416 | "node_modules/@rollup/rollup-android-arm64": { 417 | "version": "4.24.0", 418 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", 419 | "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", 420 | "cpu": [ 421 | "arm64" 422 | ], 423 | "dev": true, 424 | "license": "MIT", 425 | "optional": true, 426 | "os": [ 427 | "android" 428 | ] 429 | }, 430 | "node_modules/@rollup/rollup-darwin-arm64": { 431 | "version": "4.24.0", 432 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", 433 | "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", 434 | "cpu": [ 435 | "arm64" 436 | ], 437 | "dev": true, 438 | "license": "MIT", 439 | "optional": true, 440 | "os": [ 441 | "darwin" 442 | ] 443 | }, 444 | "node_modules/@rollup/rollup-darwin-x64": { 445 | "version": "4.24.0", 446 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", 447 | "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", 448 | "cpu": [ 449 | "x64" 450 | ], 451 | "dev": true, 452 | "license": "MIT", 453 | "optional": true, 454 | "os": [ 455 | "darwin" 456 | ] 457 | }, 458 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 459 | "version": "4.24.0", 460 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", 461 | "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", 462 | "cpu": [ 463 | "arm" 464 | ], 465 | "dev": true, 466 | "license": "MIT", 467 | "optional": true, 468 | "os": [ 469 | "linux" 470 | ] 471 | }, 472 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 473 | "version": "4.24.0", 474 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", 475 | "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", 476 | "cpu": [ 477 | "arm" 478 | ], 479 | "dev": true, 480 | "license": "MIT", 481 | "optional": true, 482 | "os": [ 483 | "linux" 484 | ] 485 | }, 486 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 487 | "version": "4.24.0", 488 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", 489 | "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", 490 | "cpu": [ 491 | "arm64" 492 | ], 493 | "dev": true, 494 | "license": "MIT", 495 | "optional": true, 496 | "os": [ 497 | "linux" 498 | ] 499 | }, 500 | "node_modules/@rollup/rollup-linux-arm64-musl": { 501 | "version": "4.24.0", 502 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", 503 | "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", 504 | "cpu": [ 505 | "arm64" 506 | ], 507 | "dev": true, 508 | "license": "MIT", 509 | "optional": true, 510 | "os": [ 511 | "linux" 512 | ] 513 | }, 514 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 515 | "version": "4.24.0", 516 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", 517 | "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", 518 | "cpu": [ 519 | "ppc64" 520 | ], 521 | "dev": true, 522 | "license": "MIT", 523 | "optional": true, 524 | "os": [ 525 | "linux" 526 | ] 527 | }, 528 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 529 | "version": "4.24.0", 530 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", 531 | "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", 532 | "cpu": [ 533 | "riscv64" 534 | ], 535 | "dev": true, 536 | "license": "MIT", 537 | "optional": true, 538 | "os": [ 539 | "linux" 540 | ] 541 | }, 542 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 543 | "version": "4.24.0", 544 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", 545 | "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", 546 | "cpu": [ 547 | "s390x" 548 | ], 549 | "dev": true, 550 | "license": "MIT", 551 | "optional": true, 552 | "os": [ 553 | "linux" 554 | ] 555 | }, 556 | "node_modules/@rollup/rollup-linux-x64-gnu": { 557 | "version": "4.24.0", 558 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", 559 | "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", 560 | "cpu": [ 561 | "x64" 562 | ], 563 | "dev": true, 564 | "license": "MIT", 565 | "optional": true, 566 | "os": [ 567 | "linux" 568 | ] 569 | }, 570 | "node_modules/@rollup/rollup-linux-x64-musl": { 571 | "version": "4.24.0", 572 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", 573 | "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", 574 | "cpu": [ 575 | "x64" 576 | ], 577 | "dev": true, 578 | "license": "MIT", 579 | "optional": true, 580 | "os": [ 581 | "linux" 582 | ] 583 | }, 584 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 585 | "version": "4.24.0", 586 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", 587 | "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", 588 | "cpu": [ 589 | "arm64" 590 | ], 591 | "dev": true, 592 | "license": "MIT", 593 | "optional": true, 594 | "os": [ 595 | "win32" 596 | ] 597 | }, 598 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 599 | "version": "4.24.0", 600 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", 601 | "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", 602 | "cpu": [ 603 | "ia32" 604 | ], 605 | "dev": true, 606 | "license": "MIT", 607 | "optional": true, 608 | "os": [ 609 | "win32" 610 | ] 611 | }, 612 | "node_modules/@rollup/rollup-win32-x64-msvc": { 613 | "version": "4.24.0", 614 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", 615 | "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", 616 | "cpu": [ 617 | "x64" 618 | ], 619 | "dev": true, 620 | "license": "MIT", 621 | "optional": true, 622 | "os": [ 623 | "win32" 624 | ] 625 | }, 626 | "node_modules/@types/estree": { 627 | "version": "1.0.6", 628 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", 629 | "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", 630 | "dev": true, 631 | "license": "MIT" 632 | }, 633 | "node_modules/esbuild": { 634 | "version": "0.21.5", 635 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", 636 | "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", 637 | "dev": true, 638 | "hasInstallScript": true, 639 | "license": "MIT", 640 | "bin": { 641 | "esbuild": "bin/esbuild" 642 | }, 643 | "engines": { 644 | "node": ">=12" 645 | }, 646 | "optionalDependencies": { 647 | "@esbuild/aix-ppc64": "0.21.5", 648 | "@esbuild/android-arm": "0.21.5", 649 | "@esbuild/android-arm64": "0.21.5", 650 | "@esbuild/android-x64": "0.21.5", 651 | "@esbuild/darwin-arm64": "0.21.5", 652 | "@esbuild/darwin-x64": "0.21.5", 653 | "@esbuild/freebsd-arm64": "0.21.5", 654 | "@esbuild/freebsd-x64": "0.21.5", 655 | "@esbuild/linux-arm": "0.21.5", 656 | "@esbuild/linux-arm64": "0.21.5", 657 | "@esbuild/linux-ia32": "0.21.5", 658 | "@esbuild/linux-loong64": "0.21.5", 659 | "@esbuild/linux-mips64el": "0.21.5", 660 | "@esbuild/linux-ppc64": "0.21.5", 661 | "@esbuild/linux-riscv64": "0.21.5", 662 | "@esbuild/linux-s390x": "0.21.5", 663 | "@esbuild/linux-x64": "0.21.5", 664 | "@esbuild/netbsd-x64": "0.21.5", 665 | "@esbuild/openbsd-x64": "0.21.5", 666 | "@esbuild/sunos-x64": "0.21.5", 667 | "@esbuild/win32-arm64": "0.21.5", 668 | "@esbuild/win32-ia32": "0.21.5", 669 | "@esbuild/win32-x64": "0.21.5" 670 | } 671 | }, 672 | "node_modules/fsevents": { 673 | "version": "2.3.3", 674 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 675 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 676 | "dev": true, 677 | "hasInstallScript": true, 678 | "license": "MIT", 679 | "optional": true, 680 | "os": [ 681 | "darwin" 682 | ], 683 | "engines": { 684 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 685 | } 686 | }, 687 | "node_modules/nanoid": { 688 | "version": "3.3.7", 689 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 690 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 691 | "dev": true, 692 | "funding": [ 693 | { 694 | "type": "github", 695 | "url": "https://github.com/sponsors/ai" 696 | } 697 | ], 698 | "license": "MIT", 699 | "bin": { 700 | "nanoid": "bin/nanoid.cjs" 701 | }, 702 | "engines": { 703 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 704 | } 705 | }, 706 | "node_modules/picocolors": { 707 | "version": "1.1.1", 708 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 709 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 710 | "dev": true, 711 | "license": "ISC" 712 | }, 713 | "node_modules/postcss": { 714 | "version": "8.4.47", 715 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", 716 | "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", 717 | "dev": true, 718 | "funding": [ 719 | { 720 | "type": "opencollective", 721 | "url": "https://opencollective.com/postcss/" 722 | }, 723 | { 724 | "type": "tidelift", 725 | "url": "https://tidelift.com/funding/github/npm/postcss" 726 | }, 727 | { 728 | "type": "github", 729 | "url": "https://github.com/sponsors/ai" 730 | } 731 | ], 732 | "license": "MIT", 733 | "dependencies": { 734 | "nanoid": "^3.3.7", 735 | "picocolors": "^1.1.0", 736 | "source-map-js": "^1.2.1" 737 | }, 738 | "engines": { 739 | "node": "^10 || ^12 || >=14" 740 | } 741 | }, 742 | "node_modules/rollup": { 743 | "version": "4.24.0", 744 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", 745 | "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", 746 | "dev": true, 747 | "license": "MIT", 748 | "dependencies": { 749 | "@types/estree": "1.0.6" 750 | }, 751 | "bin": { 752 | "rollup": "dist/bin/rollup" 753 | }, 754 | "engines": { 755 | "node": ">=18.0.0", 756 | "npm": ">=8.0.0" 757 | }, 758 | "optionalDependencies": { 759 | "@rollup/rollup-android-arm-eabi": "4.24.0", 760 | "@rollup/rollup-android-arm64": "4.24.0", 761 | "@rollup/rollup-darwin-arm64": "4.24.0", 762 | "@rollup/rollup-darwin-x64": "4.24.0", 763 | "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", 764 | "@rollup/rollup-linux-arm-musleabihf": "4.24.0", 765 | "@rollup/rollup-linux-arm64-gnu": "4.24.0", 766 | "@rollup/rollup-linux-arm64-musl": "4.24.0", 767 | "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", 768 | "@rollup/rollup-linux-riscv64-gnu": "4.24.0", 769 | "@rollup/rollup-linux-s390x-gnu": "4.24.0", 770 | "@rollup/rollup-linux-x64-gnu": "4.24.0", 771 | "@rollup/rollup-linux-x64-musl": "4.24.0", 772 | "@rollup/rollup-win32-arm64-msvc": "4.24.0", 773 | "@rollup/rollup-win32-ia32-msvc": "4.24.0", 774 | "@rollup/rollup-win32-x64-msvc": "4.24.0", 775 | "fsevents": "~2.3.2" 776 | } 777 | }, 778 | "node_modules/source-map-js": { 779 | "version": "1.2.1", 780 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 781 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 782 | "dev": true, 783 | "license": "BSD-3-Clause", 784 | "engines": { 785 | "node": ">=0.10.0" 786 | } 787 | }, 788 | "node_modules/vite": { 789 | "version": "5.4.9", 790 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", 791 | "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", 792 | "dev": true, 793 | "license": "MIT", 794 | "dependencies": { 795 | "esbuild": "^0.21.3", 796 | "postcss": "^8.4.43", 797 | "rollup": "^4.20.0" 798 | }, 799 | "bin": { 800 | "vite": "bin/vite.js" 801 | }, 802 | "engines": { 803 | "node": "^18.0.0 || >=20.0.0" 804 | }, 805 | "funding": { 806 | "url": "https://github.com/vitejs/vite?sponsor=1" 807 | }, 808 | "optionalDependencies": { 809 | "fsevents": "~2.3.3" 810 | }, 811 | "peerDependencies": { 812 | "@types/node": "^18.0.0 || >=20.0.0", 813 | "less": "*", 814 | "lightningcss": "^1.21.0", 815 | "sass": "*", 816 | "sass-embedded": "*", 817 | "stylus": "*", 818 | "sugarss": "*", 819 | "terser": "^5.4.0" 820 | }, 821 | "peerDependenciesMeta": { 822 | "@types/node": { 823 | "optional": true 824 | }, 825 | "less": { 826 | "optional": true 827 | }, 828 | "lightningcss": { 829 | "optional": true 830 | }, 831 | "sass": { 832 | "optional": true 833 | }, 834 | "sass-embedded": { 835 | "optional": true 836 | }, 837 | "stylus": { 838 | "optional": true 839 | }, 840 | "sugarss": { 841 | "optional": true 842 | }, 843 | "terser": { 844 | "optional": true 845 | } 846 | } 847 | } 848 | } 849 | } 850 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "vite": "^5.4.9" 4 | } 5 | } 6 | --------------------------------------------------------------------------------