├── .github ├── ISSUE_TEMPLATE │ ├── 01-bug.yml │ ├── 02-feature.yml │ ├── 03-documentation.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── auto-comment.yml ├── .gitignore ├── Articles.postman_collection.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── User.postman_collection.json ├── back-end ├── .gitignore ├── Config │ └── mailconfig.js ├── Utils │ ├── Api.Error.js │ ├── Api.Response.js │ ├── EmailTemplates.js │ ├── mailsender.js │ └── otpgenerate.js ├── api │ └── index.js ├── app.js ├── asset │ ├── Blogger.jpg │ ├── Reactor.jpg │ └── Reader.jpg ├── controllers │ ├── achievement.controller.js │ ├── article.controller.js │ ├── streak.controller.js │ └── user.controller.js ├── db.js ├── envexample.txt ├── middleware │ ├── achievement.middleware.js │ └── streak.middleware.js ├── models │ ├── achievement.model.js │ ├── article.model.js │ ├── level.model.js │ ├── otp.model.js │ ├── streak.model.js │ └── user.model.js ├── package-lock.json ├── package.json ├── routes │ ├── achievements.route.js │ ├── article.routes.js │ └── user.routes.js ├── utils │ └── cloudinary.js └── vercel.json ├── desktop.ini ├── front-end ├── .gitignore ├── dist │ ├── assets │ │ ├── blog-CFnBPB-Q.png │ │ ├── index-fcqxFfij.css │ │ ├── index-kpuegrC0.js │ │ ├── mission-Dc-O1fF_.jpg │ │ ├── noarticle-BJxS4Uoj.png │ │ ├── read-blog-Dfdbsgkg.jpg │ │ ├── values-DmY0X_Mx.avif │ │ ├── vision-DYlrP8Bg.avif │ │ └── write-blog-HtevdZXS.jpg │ ├── download.png │ ├── images │ │ ├── blog1.jpeg │ │ ├── blog2.jpeg │ │ └── blog3.jpeg │ ├── index.html │ └── vite.svg ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public │ ├── download.png │ ├── images │ │ ├── blog1.jpeg │ │ ├── blog2.jpeg │ │ └── blog3.jpeg │ └── logo.svg ├── src │ ├── App.css │ ├── App.jsx │ ├── Utils │ │ ├── loader.css │ │ └── loader.jsx │ ├── assets │ │ ├── Home.png │ │ ├── Logo1.png │ │ ├── blog.png │ │ ├── close.svg │ │ ├── facebook.svg │ │ ├── github.svg │ │ ├── image.png │ │ ├── instagram.svg │ │ ├── logo.png │ │ ├── menu.svg │ │ ├── mission.jpg │ │ ├── noarticle.png │ │ ├── react copy.svg │ │ ├── react.svg │ │ ├── read-blog.jpg │ │ ├── sampleBlogs.json │ │ ├── twitter.svg │ │ ├── values.avif │ │ ├── vision.avif │ │ └── write-blog.jpg │ ├── components │ │ ├── AchievementComp │ │ │ └── AchievementComp.jsx │ │ ├── AddComment.jsx │ │ ├── AlertDialog.jsx │ │ ├── ArticledetailsModal.jsx │ │ ├── Articles.jsx │ │ ├── Baselink.js │ │ ├── Centre.jsx │ │ ├── CommentsList.jsx │ │ ├── CursorTrail.jsx │ │ ├── Footer.css │ │ ├── Footer.jsx │ │ ├── ForgotPassword.jsx │ │ ├── LikeArticles │ │ │ └── LikeArticlesComp.jsx │ │ ├── LikeButton.jsx │ │ ├── Model.jsx │ │ ├── Navbar.jsx │ │ ├── PaginatedArticle.jsx │ │ ├── Preloader.css │ │ ├── Preloader.jsx │ │ ├── SaveForLaterComp │ │ │ ├── SaveforLater.jsx │ │ │ └── SaveforlaterButton.jsx │ │ ├── ScrollToTop.jsx │ │ ├── Tidio.jsx │ │ ├── contributors │ │ │ ├── ContributorsLink.jsx │ │ │ └── contribution.js │ │ └── paginatedComp │ │ │ └── ProfilePaginatedComp.jsx │ ├── hooks │ │ └── useTheme.js │ ├── index.css │ ├── index.html │ ├── main.jsx │ ├── pages │ │ ├── About.jsx │ │ ├── AddArticle.jsx │ │ ├── AddComment.jsx │ │ ├── AddarticlePage.jsx │ │ ├── Article.jsx │ │ ├── ArticleList.jsx │ │ ├── CertificateGenerator.jsx │ │ ├── Contributors.jsx │ │ ├── Dashboard.jsx │ │ ├── DraftsPage.jsx │ │ ├── EditArticle.jsx │ │ ├── EditProfilePage.jsx │ │ ├── Error404.jsx │ │ ├── FAQ.jsx │ │ ├── Home.jsx │ │ ├── NotFound.jsx │ │ ├── PrivacyPolicy.jsx │ │ ├── ProfilePage.jsx │ │ ├── PublicProfile.jsx │ │ ├── SavedArticles.jsx │ │ ├── article-content.jsx │ │ ├── article_jsx_backup.txt │ │ ├── popup.css │ │ ├── popup.html │ │ ├── popup.jsx │ │ ├── prevarticlesectionBackup.jsx │ │ ├── sampleArticles.jsx │ │ └── shareBtn.css │ └── store │ │ ├── authSlice.js │ │ └── store.js ├── tailwind.config.cjs └── vite.config.js ├── iwoc.png ├── swoc.jpg └── tailwind.config.js /.github/ISSUE_TEMPLATE/01-bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | title: "[BUG]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: what-happened 12 | attributes: 13 | label: What happened? 14 | description: Also tell us, what did you expect to happen? 15 | placeholder: Tell us what you see! 16 | value: | 17 | # Description 18 | 19 | # Steps to Reproduce 20 | - 21 | - 22 | 23 | # Expected Behavior 24 | 25 | # Actual Behavior 26 | 27 | **Select which event you're participating from** 28 | - [] Social Winter of Code 2025 (SWOC 2025) 29 | - [] Innogeeks Winter of Code 3.0 (IWOC 2025) 30 | 31 | # Screenshots 32 | 33 | validations: 34 | required: true 35 | - type: dropdown 36 | id: browsers 37 | attributes: 38 | label: What browsers are you seeing the problem on? 39 | multiple: true 40 | options: 41 | - Firefox 42 | - Chrome 43 | - Safari 44 | - Microsoft Edge 45 | - type: dropdown 46 | id: screensizes 47 | attributes: 48 | label: What screen size are you seeing the problem on? 49 | multiple: true 50 | options: 51 | - Desktop 52 | - Tablet 53 | - Mobile 54 | - type: checkboxes 55 | id: terms 56 | attributes: 57 | label: Code of Conduct 58 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/OkenHaha/react-blog/blob/main/CODE_OF_CONDUCT.md). 59 | options: 60 | - label: I agree to follow this project's Code of Conduct 61 | required: true 62 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02-feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project. 3 | title: "[FEAT]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to suggest a New Feature! 10 | - type: textarea 11 | id: new-feature 12 | attributes: 13 | label: What is the Feature/Idea? 14 | description: "A new feature!" 15 | value: | 16 | ## Is your feature request related to a problem/idea? Please describe. 17 | A clear and concise description of what the problem/idea is. 18 | 19 | ## Describe the solution you'd like 20 | A clear and concise description of what you want to happen. 21 | 22 | ## Describe alternatives you've considered 23 | A clear and concise description of any alternative solutions or features you've considered. 24 | 25 | ## Additional context 26 | Add any other context or screenshots about the feature request here. 27 | 28 | **Select which event you're participating from** 29 | - [] Social Winter of Code 2025 (SWOC 2025) 30 | - [] Innogeeks Winter of Code 3.0 (IWOC 2025) 31 | validations: 32 | required: true 33 | - type: checkboxes 34 | id: terms 35 | attributes: 36 | label: Code of Conduct 37 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/OkenHaha/react-blog/blob/main/CODE_OF_CONDUCT.md). 38 | options: 39 | - label: I agree to follow this project's Code of Conduct 40 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Suggest updates, fixes, or additions to the documentation. 3 | title: "[DOCS]: " 4 | labels: ["documentation"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to update the Documentation! 10 | - type: textarea 11 | id: docs 12 | attributes: 13 | label: What are the changes you propose? 14 | description: "A documentation upgrade!" 15 | value: | 16 | **Describe the change that is needed** 17 | A clear and concise description of what needs to be updated, fixed, or added in the documentation. 18 | 19 | **Select which event you're participating from** 20 | - [] Social Winter of Code 2025 (SWOC 2025) 21 | - [] Innogeeks Winter of Code 3.0 (IWOC 2025) 22 | 23 | **Location of the documentation** 24 | Provide links or specific sections/pages where the changes are required (e.g., README.md, docs/setup-guide.md). 25 | 26 | **Reason for the change** 27 | Explain why this change is necessary. For example: 28 | - Is the documentation incorrect? 29 | - Is there missing information? 30 | - Is the existing documentation unclear or outdated? 31 | 32 | **Suggested changes** 33 | If possible, provide a clear outline or snippet of the suggested changes. 34 | 35 | **Additional context** 36 | Add any other context, screenshots, or examples to support the documentation change request. 37 | validations: 38 | required: true 39 | - type: checkboxes 40 | id: terms 41 | attributes: 42 | label: Code of Conduct 43 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/OkenHaha/react-blog/blob/main/CODE_OF_CONDUCT.md). 44 | options: 45 | - label: I agree to follow this project's Code of Conduct 46 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: General Question 4 | url: https://github.com/OkenHaha/react-blog/discussions 5 | about: If you have a general question or need help, start a discussion here. 6 | - name: Report Security Vulnerability 7 | url: mailto:keithellakpamoken@gmail.com 8 | about: If you found a security issue, please report it confidentially using this link. 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 📝 Description 2 | Describe the changes you have done in this pull request. 3 | 4 | ## 🔗 Related Issues 5 | - Closes # 6 | - Related to # (optional) 7 | 8 | ## 📸 Screenshots / 📹 Videos 9 | Attach screenshots or screen recordings showcasing the changes, if applicable. 10 | 11 | ## ✅ Checklist 12 | - [ ] I have read and followed the [Contributing Guidelines](https://github.com/OkenHaha/react-blog/blob/main/CONTRIBUTING.md). 13 | - [ ] I have tested my changes by running it, and works as expected. 14 | - [ ] I have tested these changes in at least Chrome and Firefox (other browsers if applicable). 15 | 16 | **Select which event you're participating from** 17 | - [] Social Winter of Code 2025 (SWOC 2025) 18 | - [] Innogeeks Winter of Code 3.0 (IWOC 2025) -------------------------------------------------------------------------------- /.github/workflows/auto-comment.yml: -------------------------------------------------------------------------------- 1 | name: Automate Issue and PR Responses 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | - closed 8 | pull_request: 9 | types: 10 | - opened 11 | - closed 12 | 13 | jobs: 14 | respond-to-events: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | # Respond to new issues 22 | - name: Respond to new issues 23 | if: ${{ github.event_name == 'issues' && github.event.action == 'opened' }} 24 | run: | 25 | echo "Hey @${{ github.actor }}, Welcome to 💖react-blog! 🎊" > comment.txt 26 | echo "Thanks for opening an issue! 🙌 Please wait for the issue to be assigned." >> comment.txt 27 | echo "Happy Coding!! ✨" >> comment.txt 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | uses: peter-evans/create-or-update-comment@v3 31 | with: 32 | issue-number: ${{ github.event.issue.number }} 33 | body-path: comment.txt 34 | 35 | # Respond to closed issues 36 | - name: Respond to closed issues 37 | if: ${{ github.event_name == 'issues' && github.event.action == 'closed' }} 38 | run: | 39 | echo "Hello @${{ github.event.issue.user.login }}! Your issue #${{ github.event.issue.number }} has been closed." > comment.txt 40 | echo "Thank you for your contribution to 💖react-blog!!! 🙌" >> comment.txt 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | uses: peter-evans/create-or-update-comment@v3 44 | with: 45 | issue-number: ${{ github.event.issue.number }} 46 | body-path: comment.txt 47 | 48 | # Respond to new PRs 49 | - name: Respond to new PRs 50 | if: ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }} 51 | run: | 52 | echo "Hey @${{ github.actor }}, Welcome to 💖react-blog 🎊" > comment.txt 53 | echo "Thanks for your contribution! Your effort makes this project better. Keep it up! 🙌" >> comment.txt 54 | echo "Please wait for the PR to be reviewed." >> comment.txt 55 | echo "Happy Coding!! ✨" >> comment.txt 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | uses: peter-evans/create-or-update-comment@v3 59 | with: 60 | issue-number: ${{ github.event.pull_request.number }} 61 | body-path: comment.txt 62 | 63 | # Respond to merged PRs 64 | - name: Respond to merged PRs 65 | if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged }} 66 | run: | 67 | echo "@${{ github.event.pull_request.user.login }} Congrats, Your pull request has been successfully merged 🥳🎉" > comment.txt 68 | echo "Thank you for your contribution to 💖react-blog!!" >> comment.txt 69 | echo "Happy coding 🎊, Keep Contributing 🙌 !!!" >> comment.txt 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | uses: peter-evans/create-or-update-comment@v3 73 | with: 74 | issue-number: ${{ github.event.pull_request.number }} 75 | body-path: comment.txt 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | /back-end/.env 4 | .env -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | - Demonstrating empathy and kindness toward other people 14 | - Being respectful of differing opinions, viewpoints, and experiences 15 | - Giving and gracefully accepting constructive feedback 16 | - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | - Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | - The use of sexualized language or imagery, and sexual attention or advances of any kind 22 | - Trolling, insulting or derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others’ private information, such as a physical or email address, without their explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Enforcement Responsibilities 28 | 29 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 32 | 33 | ## Scope 34 | 35 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [keithellakpamoken@gmail.com](mailto:keithellakpamoken@gmail.com). All complaints will be reviewed and investigated promptly and fairly. 40 | 41 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 42 | 43 | ## Enforcement Guidelines 44 | 45 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 46 | 47 | ### 1. Correction 48 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 49 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 50 | 51 | ### 2. Warning 52 | **Community Impact**: A violation through a single incident or series of actions. 53 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 54 | 55 | ### 3. Temporary Ban 56 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 57 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period. During this period, no public or private interaction with the community is allowed. Violating these terms may lead to a permanent ban. 58 | 59 | ### 4. Permanent Ban 60 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 61 | **Consequence**: A permanent ban from any sort of public interaction within the community. 62 | 63 | ## Attribution 64 | 65 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html). 66 | 67 | For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). 68 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 📚 2 | 3 | Welcome to Our Project! 4 | Thank you for your interest in contributing! Please read the following guidelines carefully to ensure smooth collaboration. 5 | 6 | ## General Guidelines: 7 | **Creativity Allowed:** If you have an idea that bends the rules, feel free to submit a pull request (PR). We may still merge it! 8 | **No Build Steps:** Please avoid adding build steps (e.g., npm install). This project is intended to stay simple as a static site. 9 | **Preserve Existing Content:** Ensure you do not remove any existing content. 10 | **Code Style:** Whether your code is clean or messy, simple or complex—it’s all welcome, as long as it works. 11 | **Add Your Name:** Don’t forget to add your name to the contributorsList file. 12 | **Keep it Small:** Try to keep your pull requests small. This helps minimize merge conflicts and makes reviews easier. 13 | 14 | ## Getting Started 15 | 16 | 1. **Fork the Repository:** 17 | - Click the "Fork" button at the top right of the repository page to create your own copy of the project. 18 | 19 | 2. **Clone Your Fork:** 20 | - Clone the forked repository to your local machine. 21 | 22 | ```bash 23 | git clone https://github.com/your-username/Hacktoberfest2024.git 24 | ``` 25 | 26 | 3. **Navigate to the Project Directory:** 27 | 28 | ```bash 29 | cd Hacktoberfest2024 30 | ``` 31 | 32 | 4. **Create a New Branch:** 33 | 34 | ```bash 35 | git checkout -b my-new-branch 36 | ``` 37 | 38 | 5. **Make Your Changes:** 39 | - Add your name to `contributors/contributorsList.js` and make any other contributions. 40 | 41 | ```bash 42 | git add . 43 | ``` 44 | 45 | 6. **Commit Your Changes:** 46 | 47 | ```bash 48 | git commit -m "Relevant message" 49 | ``` 50 | 51 | 7. **Push to Your Branch:** 52 | 53 | ```bash 54 | git push origin my-new-branch 55 | ``` 56 | 57 | 8. **Create a Pull Request:** 58 | - Go to your forked repository on GitHub and create a pull request to the main repository. 59 | 1. **Add Upstream Remote:** 60 | 61 | ```bash 62 | git remote add upstream https://github.com/fineanmol/Hacktoberfest2024 63 | ``` 64 | 65 | 2. **Verify the New Remote:** 66 | 67 | ```bash 68 | git remote -v 69 | ``` 70 | 71 | 3. **Sync Your Fork with Upstream:** 72 | 73 | ```bash 74 | git fetch upstream 75 | git merge upstream/master 76 | ``` 77 | 78 | This will pull in changes from the parent repository and help you resolve any conflicts. 79 | 80 | 4. **Stay Updated:** 81 | - Regularly pull changes from the upstream repository to keep your fork updated. 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Oken Keithellakpam 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![swoc2024 Banner](./swoc.jpg) 2 | ![iwoc2025 Banner](./iwoc.png) 3 | 4 | # React Blog - Social Winter of Code 2025 & Innogeeks WInter of Code 3.0 2025🌟 5 | 6 | Welcome to **React Blog**, an exciting application selected for **Social Winter of Code 2025** (SWOC-2025) alongside with **Innogeeks Winter of Code 3.0 2025**! 7 | 8 | This project is an open-source initiative aimed at building a robust and feature-rich blogging platform. We welcome contributors from all backgrounds to collaborate and make this project a success. If you like what you see, please consider giving this repository a **star**! 💥 9 | 10 | --- 11 | 12 | ## Features 🔄 13 | - User authentication and authorization 14 | - Create, edit, and delete blog posts 15 | - Search and filter blogs 16 | - Real-time updates with modern React and MongoDB integration 17 | 18 | --- 19 | 20 | ## Tools Used 🛠️ 21 | | Tool | 22 | |----------------| 23 | | TailwindCSS | 24 | | ExpressJS | 25 | | ReactJS | 26 | | MongoDB | 27 | | NodeJS | 28 | | Vite | 29 | 30 | --- 31 | 32 | ## Getting Started ⚡ 33 | Follow these steps to set up the project locally: 34 | 35 | ### Prerequisites ⚙ 36 | Ensure you have the following installed: 37 | - **Node.js** (version 14.0 or higher recommended) 38 | - **MongoDB** (for the database) 39 | 40 | ### Installation Steps 🔍 41 | 1. Clone the repository: 42 | ```bash 43 | git clone https://github.com/OkenHaha/react-blog.git 44 | ``` 45 | 46 | 2. Navigate into the project directory: 47 | ```bash 48 | cd react-blog 49 | ``` 50 | 51 | 3. Install dependencies for both frontend and backend: 52 | ```bash 53 | cd front-end 54 | npm install 55 | 56 | cd ../back-end 57 | npm install 58 | ``` 59 | 60 | 4. Set up the `.env` file: 61 | - Navigate to the `back-end` directory. 62 | - Create a `.env` file. 63 | - Add the following environment variables to the `.env` file: 64 | ```env 65 | CONNECTION_URL="your-mongodb-connection-url" 66 | PORT=8080 67 | JWT_SECRET="your-jwt-secret" 68 | SECRET_KEY="your-secret-key" 69 | MAIL_HOST="smtp.gmail.com" 70 | MAIL_USER="your-email@example.com" 71 | MAIL_PASS="your-email-passkey" 72 | ``` 73 | 74 | - Example for local testing: 75 | ```env 76 | CONNECTION_URL="mongodb://localhost:27017" 77 | PORT=8080 78 | JWT_SECRET="test-secret" 79 | SECRET_KEY="test-secret-key" 80 | MAIL_HOST="smtp.gmail.com" 81 | MAIL_USER="test@example.com" 82 | MAIL_PASS="test-passkey" 83 | ``` 84 | 85 | 5. Change the baselink for the server 86 | - Navigate to the **components** directory inside **src** under the front-end folder 87 | - edit the `baselink.js` file 88 | - change the backend link to `http://localhost:8080` 89 | 90 | 6. Start the development server and also for the frontend: 91 | ```bash 92 | cd back-end 93 | npm run dev 94 | ``` 95 | 96 | ```bash 97 | cd front-end 98 | npm run dev 99 | ``` 100 | 101 | 7. Open the application in your browser: 102 | - Frontend: `http://127.0.0.1:5173/` 103 | - Backend: `http://localhost:8080` 104 | 105 | --- 106 | 107 | ## Contribution Guidelines 🔧 108 | We’re thrilled to have you contribute to this project! Please follow these steps to get started: 109 | 110 | 1. Star the repo 111 | 2. Fork the repository and clone it locally. 112 | 3. Create a new branch for your feature or bugfix: 113 | ```bash 114 | git checkout -b your-branch-name 115 | ``` 116 | 4. Make your changes and commit them with clear and concise commit messages. 117 | 5. Push your changes to your forked repository: 118 | ```bash 119 | git push origin your-branch-name 120 | ``` 121 | 6. Make sure the Fork isn't behind any latest commit. (This is to make sure it doesn't have merge conflict and remove new commits being made) 122 | 7. Create a pull request (PR) from your branch to the `main` branch of this repository. 123 | 8. Wait for review and feedback. 124 | 125 | ### Pro Tips 💡 126 | - Follow the code style and standards outlined in the repository. 127 | - Check the `CONTRIBUTING.md` file for detailed contribution rules. 128 | - Join discussions on issues and share your ideas! 129 | 130 | ### Labels 131 | Currently there are few labels being used for SWOC2025 and new commers to this repo needs help in explaining the labels and how they are assigned:- 132 | - `level 1` This label is assigned for issues that mostly handle with frontend and API calls 133 | - `level 2` This label is assigned for issues that requires CRUD operations to be made (e.g. adding new database schema, adding GET, POST, DELETE, etc. request to the database) 134 | - `level 3` This label is assigned for issues that requires system design, understanding the code architecture, etc. This is for high level issue that understands the whole code structure of the project. 135 | - `SWOC` This label is used to track for SWOC2025 contributions being made 136 | - `IWOC2025`This label is used to track for IWOC contributiosn being made 137 | - `Easy` This label is used for easy issues and PR for IWOC2025 138 | - `Medium` This label is used for intermediate issues and PR for IWOC2025 139 | - `Hard` This label is used for advance issues and PR for IWOC2025 140 | - `bug` `enhancement` `documentation` `good first issue` `duplicate` `help wanted` `wontfix` `invalid` are common labels issued by GitHub which will be used according to their label names 141 | 142 | My label system might look hard but this is to make sure that my contributors to this project learn and get skilled enough that they will get a well paid job for their careers. I hope this encourages you to learn more, contribute meaningful contribution for yourself and not feel discouraged by others. I'm looking forward to what amazing and exiciting contribution you can make. Cheers! 143 | 144 |

Project Contributers❤️:

145 | 146 | 147 | 148 | ## Repo Stared By: 149 | 150 | 151 | [![Stargazers repo roster for @OkenHaha/react-blog](https://reporoster.com/stars/dark/OkenHaha/react-blog)](https://github.com/OkenHaha/react-blog/stargazers) 152 | 153 | 154 | ## Repo Forked By: 155 | 156 | 157 | [![Forkers repo roster for @OkenHaha/react-blog](https://reporoster.com/forks/dark/OkenHaha/react-blog)](https://github.com/OkenHaha/react-blog/network/members) 158 | -------------------------------------------------------------------------------- /back-end/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ -------------------------------------------------------------------------------- /back-end/Config/mailconfig.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | import dotenv from 'dotenv' 3 | dotenv.config() 4 | let transporter; 5 | 6 | export const createTransporter = async() => { 7 | transporter = nodemailer.createTransport({ 8 | host: process.env.MAIL_HOST, 9 | auth : { 10 | user : process.env.MAIL_USER, 11 | pass : process.env.MAIL_PASS 12 | }}); 13 | console.log("MAIL CONNECTION SUCCESSFULL"); 14 | return transporter; 15 | } -------------------------------------------------------------------------------- /back-end/Utils/Api.Error.js: -------------------------------------------------------------------------------- 1 | // ### ApiError Utility 2 | 3 | // The `ApiError` class provides a standardized way to handle API errors in the project. 4 | 5 | // ### Features 6 | // - Consistent error structure for API responses. 7 | // - Includes `statuscode` (HTTP status code), `message`, `errors` (details), and optional `stack` (debugging information). 8 | // - Automatically captures stack traces for better debugging. 9 | 10 | // ### Parameters 11 | // - `statuscode` (number): HTTP status code (e.g., 400, 404, 500). 12 | // - `message` (string): Description of the error (default: "Something went wrong"). 13 | // - `errors` (array): Additional error details (e.g., validation errors). 14 | // - `stack` (string, optional): Custom stack trace for debugging (default: automatically captured). 15 | 16 | // ### Example Usage 17 | // ```javascript 18 | // import { ApiError } from './utils/error.js'; 19 | 20 | // // Example route using ApiError 21 | // app.get('/example', (req, res, next) => { 22 | // try { 23 | // throw new ApiError(400, 'Invalid input', ['Missing field: username']); 24 | // } catch (error) { 25 | // next(error); // Pass the error to the middleware 26 | // } 27 | // }); 28 | // ``` 29 | 30 | 31 | 32 | class ApiError extends Error { 33 | constructor( 34 | statuscode = 500, // Default to internal server error 35 | message = "Something went wrong", 36 | errors = [], 37 | stack = "" 38 | ) { 39 | super(message); 40 | 41 | // Ensure statuscode is a valid number 42 | this.statuscode = Number.isInteger(statuscode) ? statuscode : 500; 43 | this.data = null; 44 | this.message = message; 45 | this.success = false; 46 | this.errors = Array.isArray(errors) ? errors : []; 47 | 48 | if (stack) { 49 | this.stack = stack; 50 | } else { 51 | Error.captureStackTrace(this, this.constructor); 52 | } 53 | } 54 | } 55 | 56 | export { ApiError }; 57 | -------------------------------------------------------------------------------- /back-end/Utils/Api.Response.js: -------------------------------------------------------------------------------- 1 | // ### ApiResponse Utility 2 | 3 | 4 | class ApiResponse { 5 | // Constructor to initialize the response object 6 | constructor(statusCode, data, message = "Success") { 7 | this.statusCode = statusCode; // HTTP status code (e.g., 200 for success, 500 for error) 8 | this.data = data; // Data to return in the response (can be null) 9 | this.message = message; // Message providing more context (default: "Success") 10 | this.success = statusCode < 400; // Automatically set success to true for statusCode < 400, false otherwise 11 | } 12 | } 13 | 14 | // The `ApiResponse` class provides a standardized way to handle and structure API responses. 15 | 16 | // ### Features 17 | // - Consistent response structure with `statusCode`, `data`, `message`, and `success`. 18 | // - The `success` field is automatically set to `true` if `statusCode` is less than 400, indicating a successful request. 19 | // - Customizable `message` for additional context (default: "Success"). 20 | // - `statusCode`, `data`, and `message` are configurable when creating a new response. 21 | 22 | // ### Parameters 23 | // - `statusCode` (number): The HTTP status code of the response (e.g., 200 for success, 404 for not found). 24 | // - `data` (any): The main data to return in the response (can be an object, array, or null if no data is provided). 25 | // - `message` (string, optional): A message providing additional information about the response (default: "Success"). 26 | 27 | // ### Example Usage 28 | // ```javascript 29 | // import { ApiResponse } from './utils/response.js'; 30 | // 31 | // // Example route using ApiResponse 32 | // app.get('/example', (req, res) => { 33 | // try { 34 | // const data = { user: 'John Doe' }; 35 | // // Create a success response 36 | // const response = new ApiResponse(200, data, 'User data fetched successfully'); 37 | // res.json(response); 38 | // } catch (error) { 39 | // // Create an error response 40 | // const response = new ApiResponse(500, null, 'Internal Server Error'); 41 | // res.status(500).json(response); 42 | // } 43 | // }); 44 | // ``` 45 | 46 | 47 | 48 | // ### Example Responses 49 | 50 | // #### Success Response Example 51 | // ```javascript 52 | // const successResponse = new ApiResponse(200, { user: 'John Doe' }, 'User data fetched successfully'); 53 | // console.log(successResponse); 54 | // // Output: 55 | // // { 56 | // // statusCode: 200, 57 | // // data: { user: 'John Doe' }, 58 | // // message: 'User data fetched successfully', 59 | // // success: true 60 | // } 61 | // ``` 62 | 63 | // #### Error Response Example 64 | // ```javascript 65 | // const errorResponse = new ApiResponse(500, null, 'Internal Server Error'); 66 | // console.log(errorResponse); 67 | // // Output: 68 | // // { 69 | // // statusCode: 500, 70 | // // data: null, 71 | // // message: 'Internal Server Error', 72 | // // success: false 73 | // } 74 | // ``` 75 | 76 | 77 | export { ApiResponse }; 78 | -------------------------------------------------------------------------------- /back-end/Utils/mailsender.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | dotenv.config() 3 | import { createTransporter } from '../Config/mailconfig.js' 4 | 5 | let transporterPromise = createTransporter(); 6 | 7 | export const sendMail = async (email , title ,body) => { 8 | try { 9 | const transporter = await transporterPromise; 10 | const mailOptions = { 11 | from: process.env.MAIL_USER, 12 | to: email, 13 | subject: title, 14 | html: body, 15 | }; 16 | 17 | let mailResponse = await transporter.sendMail(mailOptions); 18 | console.log("MAIL RESPONSE", mailResponse); 19 | 20 | } catch (err) { 21 | console.error("Something went wrong while generating mail", err); 22 | 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /back-end/Utils/otpgenerate.js: -------------------------------------------------------------------------------- 1 | import {User} from '../models/user.model.js'; 2 | import OTP from '../models/otp.model.js'; 3 | import otpgenerator from 'otp-generator'; 4 | import { sendMail } from './mailsender.js'; 5 | import jwt from 'jsonwebtoken'; 6 | import dotenv from 'dotenv'; 7 | dotenv.config({ path: '../.env' }); 8 | import { generateEmailVerificationEmail, generateForgotPasswordEmail, generateDeleteAccountEmail } from './EmailTemplates.js'; 9 | const secretKey = process.env.SECRET_KEY; 10 | 11 | export const genrateOtp = async (req, res) => { 12 | try { 13 | const email = req.body.email; 14 | console.log(email); 15 | 16 | if (!email) { 17 | return res.status(400).json({ 18 | success: false, 19 | message: "Email is required" 20 | }); 21 | } 22 | 23 | const findUser = await User.findOne({ email }); 24 | if (findUser) { 25 | return res.status(409).json({ 26 | success: false, 27 | message: "User already registered" 28 | }); 29 | } 30 | 31 | let otp; 32 | let findOTP; 33 | 34 | do { 35 | otp = otpgenerator.generate(6, { 36 | specialChars: false, 37 | upperCaseAlphabets: false, 38 | lowerCaseAlphabets: false 39 | }); 40 | findOTP = await OTP.findOne({ otp }); 41 | } while (findOTP); 42 | 43 | const finalOTP = await OTP.create({ email, otp }); 44 | 45 | await sendMail( 46 | email, 47 | "Welcome to React Blog App - Email Verification", 48 | generateEmailVerificationEmail(otp) 49 | ); 50 | 51 | return res.status(200).json({ 52 | success: true, 53 | message: "OTP Generated", 54 | OTP: finalOTP 55 | }); 56 | 57 | } catch (err) { 58 | console.error("Error while generating OTP:", err); 59 | return res.status(500).json({ 60 | success: false, 61 | message: "Error while generating OTP", 62 | error: err.message 63 | }); 64 | } 65 | }; 66 | 67 | export const generateOTPForDelete = async (req, res) => { 68 | try { 69 | const email = req.body.email; 70 | 71 | if (!email) { 72 | return res.status(400).json({ 73 | success: false, 74 | message: "Email is required" 75 | }); 76 | } 77 | 78 | const findUser = await User.findOne({ email }); 79 | if (!findUser) { 80 | return res.status(404).json({ 81 | success: false, 82 | message: "User not found" 83 | }); 84 | } 85 | 86 | let otp; 87 | let findOTP; 88 | 89 | do { 90 | otp = otpgenerator.generate(6, { 91 | specialChars: false, 92 | upperCaseAlphabets: false, 93 | lowerCaseAlphabets: false 94 | }); 95 | findOTP = await OTP.findOne({ otp }); 96 | } while (findOTP); 97 | 98 | const finalOTP = await OTP.create({ email, otp }); 99 | 100 | await sendMail( 101 | email, 102 | "Account Deletion Request - React Blog App", 103 | generateDeleteAccountEmail(finalOTP.otp) 104 | ); 105 | 106 | return res.status(200).json({ 107 | success: true, 108 | message: "OTP Generated", 109 | OTP: finalOTP 110 | }); 111 | 112 | } catch (err) { 113 | console.error("Error while generating OTP:", err); 114 | return res.status(500).json({ 115 | success: false, 116 | message: "Error while generating OTP", 117 | error: err.message 118 | }); 119 | } 120 | } 121 | 122 | export const generateOTPForPassword = async (req, res) => { 123 | try { 124 | const { email } = req.body; 125 | console.log(email); 126 | if (!email) { 127 | return res.status(400).json({ 128 | success: false, 129 | message: "Email is required" 130 | }); 131 | } 132 | 133 | const findUser = await User.findOne({ email }); 134 | if (!findUser) { 135 | return res.status(404).json({ 136 | success: false, 137 | message: "No user found with this email" 138 | }); 139 | } 140 | 141 | let otp; 142 | let findOTP; 143 | 144 | do { 145 | otp = otpgenerator.generate(6, { 146 | specialChars: false, 147 | upperCaseAlphabets: false, 148 | lowerCaseAlphabets: false 149 | }); 150 | findOTP = await OTP.findOne({ otp }); 151 | } while (findOTP); 152 | 153 | const finalOTP = await OTP.create({ email, otp }); 154 | 155 | await sendMail( 156 | email, 157 | "Password Reset Request - React Blog App", 158 | generateForgotPasswordEmail(finalOTP.otp) 159 | ); 160 | 161 | return res.status(200).json({ 162 | success: true, 163 | message: "Password reset OTP sent to your email", 164 | OTP: finalOTP 165 | }); 166 | 167 | } catch (err) { 168 | console.error("Error while generating password reset OTP:", err); 169 | return res.status(500).json({ 170 | success: false, 171 | message: "Error while generating OTP", 172 | error: err.message 173 | }); 174 | } 175 | }; 176 | -------------------------------------------------------------------------------- /back-end/api/index.js: -------------------------------------------------------------------------------- 1 | // import { app } from "../app.js"; 2 | import { app } from "../app.js"; 3 | // import { connectDB } from "../db.js"; 4 | import { connectDB } from "../db.js"; 5 | const port = process.env.PORT || 3000 6 | 7 | try { 8 | connectDB() 9 | .then(()=>{ 10 | console.log("mongodb initialized") 11 | app.listen(port,()=>`server running at ${port}`) 12 | }) 13 | } catch (error) { 14 | console.error("cannot connect to db, check app.js trycach block") 15 | } -------------------------------------------------------------------------------- /back-end/app.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import cors from 'cors' 3 | import dotenv from 'dotenv' 4 | 5 | dotenv.config() 6 | 7 | const app = express(); 8 | // const PORT = process.env.PORT || 8080 9 | 10 | // app.use(cors({ origin: '*', credentials: true })); 11 | // uncomment the cors above when testing it locally and comment the one below. 12 | // make sure you reverse the changes being made when you about to make a PR 13 | // app.use(cors({ origin: 'https://react-blog-lake-omega.vercel.app', credentials: true })); 14 | app.use(cors({ origin: '*', credentials: true })); 15 | 16 | app.use(express.json({extend: false})); 17 | 18 | // app.listen(PORT,()=>{ 19 | // console.log(`server running on port ${PORT}`) 20 | // }) 21 | 22 | // app.use("/",(req,res)=>{res.send('express running , mongo running')}) 23 | //routes importing 24 | import {userRouter} from "./routes/user.routes.js" 25 | import { articleRouter } from './routes/article.routes.js'; 26 | import { sendMail } from './Utils/mailsender.js' 27 | import { achievementRouter } from './routes/achievements.route.js'; 28 | app.get('/',async(req,res)=>{ 29 | res.status(200).send("express") 30 | }) 31 | app.use('/sendMail', sendMail); 32 | 33 | //routes declare 34 | app.use("/api/auth",userRouter) 35 | app.use("/api/article",articleRouter) 36 | app.use("/api/achievement", achievementRouter) 37 | export {app} -------------------------------------------------------------------------------- /back-end/asset/Blogger.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/back-end/asset/Blogger.jpg -------------------------------------------------------------------------------- /back-end/asset/Reactor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/back-end/asset/Reactor.jpg -------------------------------------------------------------------------------- /back-end/asset/Reader.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/back-end/asset/Reader.jpg -------------------------------------------------------------------------------- /back-end/controllers/achievement.controller.js: -------------------------------------------------------------------------------- 1 | import { Achievement } from "../models/achievement.model.js"; 2 | import dotenv from "dotenv" 3 | import jwt from 'jsonwebtoken' 4 | import { User } from "../models/user.model.js"; 5 | 6 | dotenv.config() 7 | 8 | const secretKey = process.env.SECRET_KEY 9 | const getAchievements = async (req, res) => { 10 | try { 11 | const authHeader = req.headers.authorization; 12 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 13 | return res.status(401).json({ error: "Unauthorized access." }); 14 | } 15 | 16 | const token = authHeader.split(" ")[1]; 17 | 18 | // Decode and Verify Token 19 | let decoded; 20 | try { 21 | decoded = jwt.verify(token, secretKey); 22 | } catch (err) { 23 | console.log(err) 24 | return res.status(401).json({ error: "Maybe invalid token" }); 25 | } 26 | 27 | const userId = decoded.userId; 28 | const fetched_user = await User.findById(userId); 29 | 30 | if (!fetched_user) { 31 | return res.status(404).json({ error: "User not found." }); 32 | } 33 | console.log(fetched_user.achievements) 34 | await fetched_user.populate('achievements') 35 | console.log(fetched_user) 36 | 37 | return res.status(200).send({ success: "achievements fetched successfully", fetchedAchievements: fetched_user.achievements }) 38 | } catch (error) { 39 | console.error(error) 40 | return res.status(500).send({ error: error }) 41 | } 42 | } 43 | 44 | export {getAchievements} -------------------------------------------------------------------------------- /back-end/controllers/streak.controller.js: -------------------------------------------------------------------------------- 1 | import { Streak } from "../models/streak.model.js"; 2 | import { User } from "../models/user.model.js"; 3 | import jwt from "jsonwebtoken" 4 | 5 | 6 | const secretKey = process.env.SECRET_KEY; 7 | 8 | const updateStreak = async (userId) => { 9 | try { 10 | // Find the user's streak record 11 | let streak = await Streak.findOne({ user: userId }); 12 | console.log("entered streak", streak) 13 | if (!streak) { 14 | // If no streak exists, create a new one starting at 1 15 | streak = new Streak({ user: userId, streak: 1, lastUpdate: new Date() }); 16 | } else { 17 | const currentDate = new Date(); 18 | const lastUpdateDate = new Date(streak.lastUpdate); 19 | 20 | // Calculate the difference in days 21 | const timeDifference = currentDate.getTime() - lastUpdateDate.getTime(); 22 | const daysDifference = timeDifference / (1000 * 3600 * 24); 23 | 24 | if (daysDifference < 1) { 25 | // User already published today, do nothing 26 | console.log(`User ${userId} already published today. Streak unchanged.`); 27 | return; 28 | } else if (daysDifference < 2) { 29 | // If it's the next day, increment streak 30 | streak.streak += 1; 31 | } else { 32 | // If more than one day is missed, reset streak to 1 33 | streak.streak = 1; 34 | } 35 | } 36 | 37 | // Update last update time 38 | streak.lastUpdate = new Date(); 39 | await streak.save(); 40 | 41 | console.log(`Streak updated for user ${userId}: ${streak.streak}`); 42 | } catch (error) { 43 | console.error("Error updating streak:", error); 44 | } 45 | }; 46 | 47 | 48 | const getCurrentStrak = async (req,res) => { 49 | try { 50 | // Validate Authorization Header 51 | const authHeader = req.headers.authorization; 52 | console.log(authHeader); 53 | if (!authHeader) { 54 | console.error("Authorization header is missing."); 55 | return res.status(401).json({ error: "No token provided." }); 56 | } 57 | 58 | const token = authHeader.split(" ")[1]; 59 | if (!token) { 60 | console.error("Bearer token is missing."); 61 | return res.status(401).json({ error: "Invalid token format." }); 62 | } 63 | 64 | // Decode and Verify Token 65 | let decoded; 66 | try { 67 | decoded = jwt.verify(token, secretKey); 68 | } catch (err) { 69 | console.error("Error decoding token:", err.message); 70 | return res.status(401).json({ error: "Invalid or expired token." }); 71 | } 72 | 73 | // Find the authenticated user 74 | const userId = decoded.userId; 75 | const user = await User.findById(userId); 76 | 77 | const fetchedStreak = await Streak.findOne({ user: userId }); 78 | if (!fetchedStreak) { 79 | return res.status(404).json({ message: "Streak record not found." }); 80 | } 81 | return res.status(200).json({ streak: fetchedStreak }); 82 | } catch (error) { 83 | console.error("Error updating streak:", error); 84 | 85 | } 86 | } 87 | 88 | export { updateStreak, getCurrentStrak }; 89 | -------------------------------------------------------------------------------- /back-end/db.js: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import mongoose from 'mongoose'; 3 | 4 | // Load environment variables from the .env file 5 | dotenv.config({ path: "/.env" }); 6 | 7 | // Check if CONNECTION_URL exists in the environment variables 8 | if (!process.env.CONNECTION_URL) { 9 | throw new Error("Missing CONNECTION_URL in environment variables"); 10 | } 11 | 12 | console.log("This is env variable:", process.env.CONNECTION_URL); 13 | 14 | const connectDB = async () => { 15 | try { 16 | const resp = await mongoose.connect(process.env.CONNECTION_URL); 17 | if (resp) { 18 | console.log("MongoDB connected"); 19 | } 20 | } catch (error) { 21 | console.log(error); 22 | } 23 | }; 24 | 25 | export { connectDB }; -------------------------------------------------------------------------------- /back-end/envexample.txt: -------------------------------------------------------------------------------- 1 | CONNECTION_URL="mongodb+srv://arkabasak62:1234@cluster0.i3bju.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0" 2 | PORT = 8080 3 | JWT_SECRET = "Circle" 4 | SECRET_KEY="57d494cad2fd6e437f2b74e2292422b39355698090da1578a420960abc42e3e9ca487adaf47662af1c311cfacacf7951bf5f8a3458ff92728da78c4868011b6a7fa81ba91d1b804e44a98b034414710b772ef7a9729c03ea4663761688b25ebea28d350164966f313daaf13b37deb8e5a1dced02dc932d0460c5c56db59df5c8c28c4994ddb9b21e471e6431639fdc3a247e41a44428e488e0dcd347786e87ab" 5 | MAIL_HOST = smtp.gmail.com 6 | MAIL_USER = replace with your email 7 | MAIL_PASS = replace with your email passkey 8 | -------------------------------------------------------------------------------- /back-end/middleware/achievement.middleware.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { User } from "../models/user.model.js"; 3 | import { Achievement } from "../models/achievement.model.js"; 4 | 5 | const secretKey = process.env.SECRET_KEY; 6 | 7 | /** 8 | * Generic middleware to check and add achievements 9 | * @param {string} achievementName - The name of the achievement 10 | * @param {string} userField - The field to check (e.g., 'articlesPublished', 'commentedArticles', 'likedArticles') 11 | * @param {number} requiredCount - The count required to earn the achievement 12 | */ 13 | const Check_add_achievement_generic = (achievementName, userField, requiredCount) => { 14 | return async (req, res, next) => { 15 | try { 16 | // Validate Authorization Header 17 | const authHeader = req.headers.authorization; 18 | if (!authHeader || !authHeader.startsWith("Bearer ")) { 19 | return res.status(401).json({ error: "Unauthorized access." }); 20 | } 21 | 22 | const token = authHeader.split(" ")[1]; 23 | 24 | // Decode and Verify Token 25 | let decoded; 26 | try { 27 | decoded = jwt.verify(token, secretKey); 28 | } catch (err) { 29 | return res.status(401).json({ error: "Invalid or expired token." }); 30 | } 31 | 32 | const userId = decoded.userId; 33 | const fetched_user = await User.findById(userId); 34 | 35 | if (!fetched_user) { 36 | return res.status(404).json({ error: "User not found." }); 37 | } 38 | console.log(fetched_user.achievements) 39 | await fetched_user.populate('achievements') 40 | console.log(fetched_user) 41 | // Achievement Update Section 42 | if (!fetched_user.achievements.includes({name:achievementName})) { 43 | console.log("entered ifblock for liked article") 44 | console.log(fetched_user[userField].length) 45 | if (fetched_user[userField].length >= requiredCount) { 46 | console.log("enterde inner ") 47 | let imageLink; 48 | if (achievementName == "Blogger") { 49 | imageLink = "https://res.cloudinary.com/djfhwhtyy/image/upload/v1739444575/r2l5abcbzeaxvxlt4bmq.png" 50 | }else if(achievementName == "Commenter"){ 51 | imageLink = "https://res.cloudinary.com/djfhwhtyy/image/upload/v1739444575/xabqfgt1ttwq1iuipob9.png" 52 | }else if (achievementName == "Reactor") { 53 | imageLink= "https://res.cloudinary.com/djfhwhtyy/image/upload/v1739444575/hgzwiazf2pxcytpgdyue.png" 54 | } 55 | const new_achievement = new Achievement({ 56 | name: achievementName, 57 | user: fetched_user._id, 58 | achievedOn: Date.now(), 59 | image: imageLink 60 | }); 61 | 62 | await new_achievement.save(); 63 | fetched_user.achievements.push(new_achievement._id); 64 | await fetched_user.save(); 65 | } 66 | } 67 | 68 | next(); 69 | } catch (error) { 70 | console.error("Internal Server Error:", error); 71 | res.status(500).json({ error: "Internal Server Error." }); 72 | } 73 | }; 74 | }; 75 | 76 | // Define specific middleware using the generic function 77 | const Check_add_achievement = Check_add_achievement_generic("Blogger", "articlesPublished", 9); 78 | const Check_add_achievement_comments = Check_add_achievement_generic("Commenter", "commentedArticles", 9); 79 | const Check_add_achievement_liked = Check_add_achievement_generic("Reactor", "likedArticles", 15); 80 | 81 | export { Check_add_achievement, Check_add_achievement_comments, Check_add_achievement_liked }; 82 | -------------------------------------------------------------------------------- /back-end/middleware/streak.middleware.js: -------------------------------------------------------------------------------- 1 | import { Streak } from "../models/streak.model.js"; 2 | import { User } from "../models/user.model.js"; 3 | 4 | // Middleware to check user's streak and reset if necessary 5 | const checkStreak = async (req, res, next) => { 6 | try { 7 | const { credential, password } = req.body; 8 | 9 | // Input validation 10 | if (!credential || !password) { 11 | console.error("Missing required fields: credential or password"); 12 | return res.status(400).json({ error: "Both credential (email/username) and password are required" }); 13 | } 14 | 15 | // Determine if credential is email or username 16 | const isEmail = credential.includes('@'); 17 | 18 | // Fetch user from database based on either email or username 19 | const user = await User.findOne( 20 | isEmail ? { email: credential } : { username: credential } 21 | ); 22 | 23 | const u_id = user._id; 24 | if (!u_id) { 25 | return res.status(400).json({ message: "User ID is required" }); 26 | } 27 | 28 | // Find user's streak record 29 | let streak = await Streak.findOne({ user: u_id }); 30 | 31 | // If no streak record exists, create one with streak 0 32 | if (!streak) { 33 | streak = new Streak({ user: u_id, streak: 0, lastUpdate: new Date() }); 34 | await streak.save(); 35 | return next(); // No need to reset if it's the first streak entry 36 | } 37 | 38 | // Get the current date and last update date 39 | const currentDate = new Date(); 40 | const lastUpdateDate = new Date(streak.lastUpdate); 41 | 42 | // Calculate the difference in days between last update and today 43 | const timeDifference = currentDate.getTime() - lastUpdateDate.getTime(); 44 | const daysDifference = timeDifference / (1000 * 3600 * 24); // Convert ms to days 45 | 46 | if (daysDifference >= 2) { 47 | // If more than 1 day has passed, reset the streak 48 | console.log(`User ${u_id} missed a day. Resetting streak.`); 49 | streak.streak = 0; 50 | } 51 | 52 | // Update last checked date 53 | streak.lastUpdate = currentDate; 54 | console.log(streak) 55 | await streak.save(); 56 | 57 | next(); // Move to next middleware or route handler 58 | } catch (error) { 59 | console.error("Error checking streak:", error); 60 | res.status(500).json({ message: "Error checking streak", error: error.message }); 61 | } 62 | }; 63 | 64 | const checkStreakArticle = async (req,res, next) => { 65 | try { 66 | const { credential, password } = req.body; 67 | 68 | // Input validation 69 | if (!credential || !password) { 70 | console.error("Missing required fields: credential or password"); 71 | return res.status(400).json({ error: "Both credential (email/username) and password are required" }); 72 | } 73 | 74 | // Determine if credential is email or username 75 | const isEmail = credential.includes('@'); 76 | 77 | // Fetch user from database based on either email or username 78 | const user = await User.findOne( 79 | isEmail ? { email: credential } : { username: credential } 80 | ); 81 | 82 | const u_id = user._id; 83 | if (!u_id) { 84 | return res.status(400).json({ message: "User ID is required" }); 85 | } 86 | 87 | // Find user's streak record 88 | let streak = await Streak.findOne({ user: u_id }); 89 | 90 | // If no streak record exists, create one with streak 0 91 | if (!streak) { 92 | streak = new Streak({ user: u_id, streak: 0, lastUpdate: new Date() }); 93 | await streak.save(); 94 | return next(); // No need to reset if it's the first streak entry 95 | } 96 | 97 | // Get the current date and last update date 98 | const currentDate = new Date(); 99 | const lastUpdateDate = new Date(streak.lastUpdate); 100 | 101 | // Calculate the difference in days between last update and today 102 | const timeDifference = currentDate.getTime() - lastUpdateDate.getTime(); 103 | const daysDifference = timeDifference / (1000 * 3600 * 24); // Convert ms to days 104 | 105 | if (daysDifference >= 2) { 106 | // If more than 1 day has passed, reset the streak 107 | console.log(`User ${u_id} missed a day. Resetting streak.`); 108 | streak.streak = 0; 109 | } 110 | 111 | // Update last checked date 112 | streak.lastUpdate = currentDate; 113 | console.log(streak) 114 | await streak.save(); 115 | 116 | next(); // Move to next middleware or route handler 117 | } catch (error) { 118 | console.error("Error checking streak:", error); 119 | res.status(500).json({ message: "Error checking streak", error: error.message }); 120 | } 121 | } 122 | 123 | export { checkStreak, checkStreakArticle }; 124 | -------------------------------------------------------------------------------- /back-end/models/achievement.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const achievementSchema = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true 7 | }, 8 | user: { 9 | type: mongoose.Schema.Types.ObjectId, 10 | ref: "User", 11 | required: true 12 | }, 13 | image:{ 14 | type: String 15 | }, 16 | achevedOn: { 17 | type: Date, 18 | default: Date.now 19 | } 20 | }, { timestamps: true }); 21 | 22 | const Achievement = mongoose.model("Achievement", achievementSchema); 23 | 24 | export { Achievement }; 25 | -------------------------------------------------------------------------------- /back-end/models/article.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | /* 3 | Predefined Tags 4 | Technology 5 | Gaming 6 | Music 7 | Movies 8 | */ 9 | const commentSchema = new mongoose.Schema({ 10 | username: { type: String, required: true }, 11 | text: { type: String, required: true }, 12 | article: { type: mongoose.Schema.Types.ObjectId, ref: 'Article', required: true }, 13 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, 14 | },{timestamps:true}); 15 | 16 | const Comment = mongoose.model('Comment', commentSchema); 17 | 18 | const articleSchema = new mongoose.Schema({ 19 | name: { type: String, required: true, unique: true }, 20 | title: { type: String, required: true }, 21 | content: { type: String, required: true }, 22 | thumbnail: { type: String, required: true }, 23 | author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, 24 | authorName:{type: String}, 25 | comments: [ 26 | { type: mongoose.Schema.Types.ObjectId, ref: 'Comment' } 27 | ], 28 | likes: { type: Number, default: 0 }, 29 | likedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], 30 | tag:{type:String}, 31 | status: { 32 | type: String, 33 | enum: ['draft', 'published'], 34 | default: 'draft' 35 | }, 36 | reactions: [{ 37 | emoji: { 38 | type: String, 39 | enum: ['👍', '❤️', '😂', '😮', '😢', '😡'] 40 | }, 41 | user: { 42 | type: mongoose.Schema.Types.ObjectId, 43 | ref: 'User' 44 | } 45 | }], 46 | },{timestamps:true}); 47 | 48 | articleSchema.methods.getReactionCounts = function() { 49 | const counts = {}; 50 | this.reactions.forEach(reaction => { 51 | counts[reaction.emoji] = (counts[reaction.emoji] || 0) + 1; 52 | }); 53 | return counts; 54 | }; 55 | 56 | const Article = mongoose.model('Article', articleSchema); 57 | 58 | export { Comment, Article }; -------------------------------------------------------------------------------- /back-end/models/level.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | 4 | const levelSchema = new mongoose.Schema({ 5 | levelName:{ 6 | type:String, 7 | default: "Beginner" 8 | }, 9 | levelProgress:{ 10 | type:Number, 11 | default:0, 12 | }, 13 | levelTotalNumber:{ 14 | type:Number, 15 | default:100 16 | }, 17 | authorId:{ 18 | type: mongoose.Schema.Types.ObjectId , 19 | ref: 'User' 20 | }, 21 | achievedOn:{ 22 | type:Date, 23 | default: Date.now, 24 | required: true 25 | } 26 | }, {timestamps:true}) 27 | 28 | const Level = mongoose.model("Level", levelSchema); 29 | 30 | export {Level} -------------------------------------------------------------------------------- /back-end/models/otp.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const OTPSchema = new mongoose.Schema({ 4 | email: { 5 | type: String, 6 | required: true, 7 | }, 8 | otp: { 9 | type: String, 10 | required: true, 11 | }, 12 | createdAt: { 13 | type: Date, 14 | default: Date.now, 15 | expires: 60 * 10, 16 | } 17 | }); 18 | 19 | export default mongoose.model("OTP", OTPSchema); -------------------------------------------------------------------------------- /back-end/models/streak.model.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const streakSchema = new mongoose.Schema({ 4 | user: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: 'User', 7 | required: true, 8 | }, 9 | streak: { 10 | type: Number, 11 | required: true, 12 | default: 0, 13 | }, 14 | lastUpdate: { 15 | type: Date, 16 | required: true, 17 | default: Date.now, 18 | }, 19 | }); 20 | 21 | const Streak = mongoose.model('Streak', streakSchema); 22 | 23 | export { Streak }; -------------------------------------------------------------------------------- /back-end/models/user.model.js: -------------------------------------------------------------------------------- 1 | import { Schema } from "mongoose"; 2 | import mongoose from "mongoose"; 3 | 4 | const userSchema = new Schema( 5 | { 6 | username: { 7 | type: String, 8 | required: true 9 | }, 10 | email: { 11 | type: String, 12 | required: true 13 | }, 14 | password: { 15 | type: String, 16 | required: true 17 | }, 18 | name: { 19 | type: String, 20 | required: false 21 | }, 22 | location: { 23 | type: String, 24 | required: false 25 | }, 26 | picture: { 27 | type: String, 28 | required: false 29 | }, 30 | dob: { 31 | type: Date, 32 | required: false 33 | }, 34 | age: { 35 | type: Number, 36 | required: false, 37 | default: function () { 38 | if (this.dob) { 39 | const ageDifMs = Date.now() - new Date(this.dob).getTime(); 40 | const ageDate = new Date(ageDifMs); 41 | return Math.abs(ageDate.getUTCFullYear() - 1970); 42 | } 43 | return null; 44 | } 45 | }, 46 | accountCreated: { 47 | type: Date, 48 | required: true, 49 | default: Date.now 50 | }, 51 | articlesPublished: { 52 | type: Number, 53 | required: false, 54 | default: 0 55 | }, 56 | isEmailVerified: { 57 | type: Boolean, 58 | default: false 59 | }, 60 | likedArticles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Article' }], 61 | commentedArticles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Article' }], 62 | saveForLater:[{type: mongoose.Schema.Types.ObjectId, ref: 'Article'}], 63 | draftArticles:[{type: mongoose.Schema.Types.ObjectId, ref: 'Article'}], 64 | authorLevel :{ 65 | type: mongoose.Schema.Types.ObjectId, 66 | ref: 'Level' 67 | }, 68 | achievements:[{ 69 | type:mongoose.Schema.Types.ObjectId, 70 | ref:"Achievement" 71 | }], 72 | followers: [{ 73 | type: mongoose.Schema.Types.ObjectId, 74 | ref: 'User' 75 | }], 76 | following: [{ 77 | type: mongoose.Schema.Types.ObjectId, 78 | ref: 'User' 79 | }] 80 | },{timestamps: true} 81 | ); 82 | 83 | const User = mongoose.model("User", userSchema); 84 | 85 | export { User }; -------------------------------------------------------------------------------- /back-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-backend", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "node ./api/index.js", 7 | "dev": "nodemon ./api/index.js" 8 | }, 9 | "engines": { 10 | "node": ">=14.0.0" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.7.2", 14 | "bcrypt": "^5.1.1", 15 | "cloudinary": "^2.5.1", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.3.1", 18 | "express": "^4.18.1", 19 | "jsonwebtoken": "^9.0.2", 20 | "mongoose": "^6.0.12", 21 | "multer": "^1.4.5-lts.1", 22 | "nodemailer": "^6.9.16", 23 | "otp-generator": "^4.0.1", 24 | "punycode": "^2.3.1", 25 | "streamifier": "^0.1.1" 26 | }, 27 | "devDependencies": { 28 | "nodemon": "^2.0.19" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /back-end/routes/achievements.route.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { getAchievements } from "../controllers/achievement.controller.js"; 3 | 4 | const achievementRouter= express.Router() 5 | 6 | achievementRouter.get("/getachievements",getAchievements) 7 | 8 | export {achievementRouter} -------------------------------------------------------------------------------- /back-end/routes/article.routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getarticles, addcomments,addArticle, getAllArticles, editArticle, getarticlebyid, deleteArticle, getarticlesbyuser, likeArticle, getArticleByTag, saveforlater, saveasdraft, getUserDrafts, getsaveforlater, removeSaveforLater, recentComments, addReaction, removeReaction, getReactions } from "../controllers/article.controller.js"; 3 | import multer from 'multer' 4 | import { upload_on_cloudinary } from "../utils/cloudinary.js"; 5 | import { Check_add_achievement, Check_add_achievement_comments, Check_add_achievement_liked } from "../middleware/achievement.middleware.js"; 6 | 7 | const storage = multer.memoryStorage(); 8 | const upload = multer({ storage: storage }); 9 | 10 | const articleRouter = Router() 11 | 12 | // add article route 13 | articleRouter.post('/addarticle', Check_add_achievement, upload.single("thumbnail"), addArticle); 14 | 15 | // get article route 16 | articleRouter.post('/getarticle', getarticles); 17 | 18 | // get all article route 19 | articleRouter.get('/getallarticle', getAllArticles); 20 | 21 | //get article by specfic user 22 | articleRouter.post("/getarticlesbyuser", getarticlesbyuser) 23 | 24 | // add comment route 25 | articleRouter.post('/addcomment', Check_add_achievement_comments, addcomments); 26 | 27 | //edit article 28 | articleRouter.post('/editarticle', upload.single("thumbnail") ,editArticle) 29 | 30 | //get article by id 31 | articleRouter.post('/getarticlebyid', getarticlebyid) 32 | 33 | // delete article 34 | articleRouter.delete('/deletearticle', deleteArticle); 35 | 36 | // Like/Unlike article route 37 | articleRouter.post('/like/:articleId', Check_add_achievement_liked, likeArticle); 38 | 39 | articleRouter.post('/getarticlebytag',getArticleByTag); 40 | 41 | 42 | //add to save for later 43 | articleRouter.post('/saveforlater', saveforlater) 44 | 45 | //save as draft 46 | articleRouter.post('/create-draft', upload.single('thumbnail') ,saveasdraft) 47 | 48 | //get user drafts 49 | articleRouter.get('/drafts', getUserDrafts) 50 | 51 | //get save for later articles 52 | articleRouter.get('/getsavedlarticles', getsaveforlater) 53 | 54 | //remove a article from savefor later 55 | articleRouter.post('/removeSavedArticle', removeSaveforLater) 56 | 57 | //get recent comments 58 | articleRouter.get('/getrecentomment/:id', recentComments) 59 | 60 | // Add reaction routes 61 | articleRouter.post('/react/:articleId', addReaction); 62 | articleRouter.delete('/react/:articleId', removeReaction); 63 | articleRouter.get('/reactions/:articleId', getReactions); 64 | 65 | export { articleRouter }; -------------------------------------------------------------------------------- /back-end/routes/user.routes.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { loginUser, registerUser, getProfile, editProfile, deleteUserAccount, resetPassword, getOtherUser, followUser, unfollowUser } from "../controllers/user.controller.js"; 3 | import { genrateOtp, generateOTPForDelete, generateOTPForPassword } from '../Utils/otpgenerate.js'; 4 | import multer from 'multer' 5 | import { checkStreak } from '../middleware/streak.middleware.js'; 6 | import { getCurrentStrak } from '../controllers/streak.controller.js'; 7 | 8 | 9 | const upload = multer({storage: multer.memoryStorage()}) 10 | 11 | 12 | const userRouter = express.Router(); 13 | 14 | // Register route 15 | userRouter.post('/register/generate-otp', genrateOtp); 16 | userRouter.post('/register', registerUser); 17 | 18 | // Login route 19 | userRouter.post('/login', checkStreak, loginUser); 20 | 21 | // Profile route 22 | userRouter.get('/getProfile', getProfile); 23 | 24 | // Edit profile route 25 | userRouter.post('/editProfile', upload.single("picture") , editProfile); 26 | 27 | // Delete account route 28 | userRouter.post('/deleteAccount/generate-otp', generateOTPForDelete); 29 | userRouter.delete('/deleteAccount', deleteUserAccount); 30 | 31 | // Forgot Password routes 32 | userRouter.post('/forgot-password/generate-otp', generateOTPForPassword); 33 | userRouter.post('/reset-password', resetPassword); 34 | 35 | // Get other user's profile 36 | userRouter.get('/user/:userId', getOtherUser); 37 | 38 | // Follow/Unfollow routes 39 | userRouter.post('/follow/:userToFollowId', followUser); 40 | userRouter.post('/unfollow/:userToUnfollowId', unfollowUser); 41 | 42 | 43 | //get courrent streak 44 | userRouter.get('/streak', getCurrentStrak) 45 | 46 | export { userRouter }; 47 | -------------------------------------------------------------------------------- /back-end/utils/cloudinary.js: -------------------------------------------------------------------------------- 1 | // import {v2 as cloudinary} from 'cloudinary' 2 | 3 | 4 | // const upload_on_cloudinary = async (filepath) => { 5 | // // Configuration 6 | // cloudinary.config({ 7 | // cloud_name: 'djfhwhtyy', 8 | // api_key: '944476654513192', 9 | // api_secret: 'AbbLeYlaOpNfB1lHWeJACHmJGlg' // Click 'View API Keys' above to copy your API secret 10 | // }); 11 | 12 | // if (!filepath) { 13 | // console.log("file path is not upadating ") 14 | // return null 15 | // } 16 | 17 | // const upload_res = await cloudinary.uploader.upload(filepath) 18 | // return upload_res 19 | 20 | // } 21 | 22 | // export {upload_on_cloudinary} 23 | 24 | // import { v2 as cloudinary } from "cloudinary"; 25 | 26 | // // Cloudinary configuration 27 | // cloudinary.config({ 28 | // cloud_name: "your_cloud_name", 29 | // api_key: "your_api_key", 30 | // api_secret: "your_api_secret", 31 | // secure: true, 32 | // }); 33 | 34 | // const uploadToCloudinary = async (file) => { 35 | // try { 36 | // if (!file || !(file instanceof File || file instanceof Blob)) { 37 | // throw new Error("Invalid file. Ensure you provide a valid File or Blob object."); 38 | // } 39 | 40 | // // Read file as a base64 string 41 | // const base64Data = await new Promise((resolve, reject) => { 42 | // const reader = new FileReader(); 43 | // reader.onload = () => resolve(reader.result.split(",")[1]); // Exclude `data:mime;base64,` 44 | // reader.onerror = (err) => reject(err); 45 | // reader.readAsDataURL(file); 46 | // }); 47 | 48 | // // Generate Data URI for Cloudinary 49 | // const mime = file.type; 50 | // const fileUri = `data:${mime};base64,${base64Data}`; 51 | 52 | // // Upload to Cloudinary 53 | // const result = await cloudinary.uploader.upload(fileUri, { invalidate: true }); 54 | 55 | // // Return secure URL 56 | // return result.secure_url; 57 | // } catch (error) { 58 | // console.error("Error uploading to Cloudinary:", error); 59 | // throw error; 60 | // } 61 | // }; 62 | 63 | // export { uploadToCloudinary }; 64 | 65 | // const { v2: cloudinary } = require('cloudinary'); 66 | import {v2 as cloudinary} from 'cloudinary' 67 | import streamifier from 'streamifier' 68 | // const streamifier = require('streamifier'); 69 | 70 | // Cloudinary configuration 71 | cloudinary.config({ 72 | cloud_name: 'djfhwhtyy', 73 | api_key: '944476654513192', 74 | api_secret: 'AbbLeYlaOpNfB1lHWeJACHmJGlg', 75 | secure: true, 76 | }); 77 | 78 | const upload_on_cloudinary = async (fileBuffer, folderName = "demo") => { 79 | try { 80 | if (!fileBuffer) { 81 | console.log("No file buffer provided"); 82 | return null; 83 | } 84 | 85 | return new Promise((resolve, reject) => { 86 | const stream = cloudinary.uploader.upload_stream( 87 | { 88 | folder: folderName, // Optional folder in Cloudinary 89 | }, 90 | (error, result) => { 91 | if (error) { 92 | console.error("Cloudinary upload error:", error); 93 | reject(error); 94 | } else { 95 | resolve(result.secure_url); // Return the secure URL of the uploaded image 96 | } 97 | } 98 | ); 99 | 100 | // Pipe the file buffer to Cloudinary's upload stream 101 | streamifier.createReadStream(fileBuffer).pipe(stream); 102 | }); 103 | } catch (error) { 104 | console.error("Error during Cloudinary upload:", error); 105 | throw error; 106 | } 107 | }; 108 | 109 | export { upload_on_cloudinary }; 110 | -------------------------------------------------------------------------------- /back-end/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "api/index.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "api/index.js", 13 | "methods": [ 14 | "GET", 15 | "POST", 16 | "PUT", 17 | "DELETE", 18 | "PATCH", 19 | "OPTIONS" 20 | ] 21 | }, 22 | { 23 | "src": "/api/(.*)", 24 | "headers": { 25 | "Access-Control-Allow-Credentials": "true", 26 | "Access-Control-Allow-Origin": "*", 27 | "Access-Control-Allow-Methods": "GET,OPTIONS,PATCH,DELETE,POST,PUT", 28 | "Access-Control-Allow-Headers": "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version" 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /desktop.ini: -------------------------------------------------------------------------------- 1 | [.ShellClassInfo] 2 | LocalizedResourceName=@react-blog,0 3 | -------------------------------------------------------------------------------- /front-end/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /front-end/dist/assets/blog-CFnBPB-Q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/assets/blog-CFnBPB-Q.png -------------------------------------------------------------------------------- /front-end/dist/assets/mission-Dc-O1fF_.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/assets/mission-Dc-O1fF_.jpg -------------------------------------------------------------------------------- /front-end/dist/assets/noarticle-BJxS4Uoj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/assets/noarticle-BJxS4Uoj.png -------------------------------------------------------------------------------- /front-end/dist/assets/read-blog-Dfdbsgkg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/assets/read-blog-Dfdbsgkg.jpg -------------------------------------------------------------------------------- /front-end/dist/assets/values-DmY0X_Mx.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/assets/values-DmY0X_Mx.avif -------------------------------------------------------------------------------- /front-end/dist/assets/vision-DYlrP8Bg.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/assets/vision-DYlrP8Bg.avif -------------------------------------------------------------------------------- /front-end/dist/assets/write-blog-HtevdZXS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/assets/write-blog-HtevdZXS.jpg -------------------------------------------------------------------------------- /front-end/dist/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/download.png -------------------------------------------------------------------------------- /front-end/dist/images/blog1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/images/blog1.jpeg -------------------------------------------------------------------------------- /front-end/dist/images/blog2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/images/blog2.jpeg -------------------------------------------------------------------------------- /front-end/dist/images/blog3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/dist/images/blog3.jpeg -------------------------------------------------------------------------------- /front-end/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Blog 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /front-end/dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front-end/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React Blog 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /front-end/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-project", 3 | "private": true, 4 | "version": "0.0.0", 5 | "proxy": "https://react-blog-onlk.onrender.com", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "start": "vite", 10 | "build": "vite build", 11 | "preview": "vite preview" 12 | }, 13 | "engines": { 14 | "node": "22.x" 15 | }, 16 | "dependencies": { 17 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 18 | "@fortawesome/free-brands-svg-icons": "^6.7.2", 19 | "@fortawesome/free-regular-svg-icons": "^6.7.2", 20 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 21 | "@fortawesome/react-fontawesome": "^0.2.2", 22 | "@headlessui/react": "^2.2.0", 23 | "@heroicons/react": "^2.2.0", 24 | "@reduxjs/toolkit": "^2.5.0", 25 | "@uiw/react-md-editor": "^4.0.5", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "axios": "^1.7.7", 28 | "chart.js": "^4.4.7", 29 | "date-fns": "^4.1.0", 30 | "dotenv": "^16.4.7", 31 | "framer-motion": "^11.18.1", 32 | "gsap": "^3.12.7", 33 | "html-to-image": "^1.11.11", 34 | "jsonwebtoken": "^9.0.2", 35 | "lucide-react": "^0.475.0", 36 | "react": "^18.2.0", 37 | "react-animated-cursor": "^2.11.2", 38 | "react-chartjs-2": "^5.3.0", 39 | "react-confetti": "^6.2.2", 40 | "react-dom": "^18.2.0", 41 | "react-icons": "^5.4.0", 42 | "react-markdown": "^9.0.3", 43 | "react-modal": "^3.16.3", 44 | "react-redux": "^9.2.0", 45 | "react-router-dom": "^6.28.1", 46 | "react-scripts": "^3.0.1", 47 | "react-toastify": "^11.0.2", 48 | "react-use": "^17.6.0", 49 | "redux-persist": "^6.0.0" 50 | }, 51 | "devDependencies": { 52 | "@types/react": "^18.0.17", 53 | "@types/react-dom": "^18.0.6", 54 | "autoprefixer": "^10.4.9", 55 | "postcss": "^8.4.16", 56 | "tailwindcss": "^3.1.8", 57 | "vite": "^6.0.11", 58 | "vite-plugin-node-polyfills": "^0.23.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /front-end/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /front-end/public/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/public/download.png -------------------------------------------------------------------------------- /front-end/public/images/blog1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/public/images/blog1.jpeg -------------------------------------------------------------------------------- /front-end/public/images/blog2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/public/images/blog2.jpeg -------------------------------------------------------------------------------- /front-end/public/images/blog3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OkenHaha/react-blog/ba6ffc008eb2732137b836517ef51a9b12c918ab/front-end/public/images/blog3.jpeg -------------------------------------------------------------------------------- /front-end/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, Suspense, lazy } from "react"; 2 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import { toggleTheme as toggle } from "./store/authSlice"; 5 | import Navbar from "./components/Navbar"; 6 | import Footer from "./components/Footer"; 7 | import ScrollToTop from "./components/ScrollToTop"; 8 | import { LoadingSpinner } from "./Utils/loader"; 9 | import AchievementPage from "./components/AchievementComp/AchievementComp"; 10 | import SavedArticles from "./pages/SavedArticles"; 11 | import Preloader from "./components/Preloader"; 12 | 13 | // Lazy load components 14 | const Home = lazy(() => import("./pages/Home")); 15 | const About = lazy(() => import("./pages/About")); 16 | const ArticleList = lazy(() => import("./pages/ArticleList")); 17 | const Article = lazy(() => import("./pages/Article")); 18 | const NotFound = lazy(() => import("./pages/NotFound")); 19 | const ProfilePage = lazy(() => import("./pages/ProfilePage")); 20 | const EditProfilePage = lazy(() => import("./pages/EditProfilePage")); 21 | const EditArticle = lazy(() => import("./pages/EditArticle")); 22 | const FAQ = lazy(() => import("./pages/FAQ")); 23 | const ForgotPassword = lazy(() => import("./components/ForgotPassword")); 24 | const Contributors = lazy(() => import("./pages/Contributors")); 25 | const Dashboard = lazy(() => import("./pages/Dashboard")); 26 | const AddarticlePage = lazy(() => import("./pages/AddarticlePage")); 27 | const Error404 = lazy(() => import("./pages/Error404")); 28 | const PublicProfile = lazy(() => import("./pages/PublicProfile")); 29 | const PrivacyPolicy = lazy(() => import("./pages/PrivacyPolicy")); 30 | const DraftsPage = lazy(() => import("./pages/DraftsPage")); 31 | 32 | 33 | function App() { 34 | const [loading, setLoading] = useState(true); 35 | const theme = useSelector((state) => state.auth.theme); 36 | const loggedInUser = useSelector((state) => state.auth.user); // Get the logged-in user's data 37 | const loggedInUserId = loggedInUser ? loggedInUser._id : null; // Extract user ID if logged in 38 | const dispatch = useDispatch(); 39 | 40 | useEffect(() => { 41 | document.documentElement.className = theme; 42 | }, [theme]); 43 | 44 | useEffect(() => { 45 | const timer = setTimeout(() => { 46 | setLoading(false); 47 | }, 2500); 48 | 49 | return () => clearTimeout(timer); 50 | }, []); 51 | 52 | const toggleTheme = () => { 53 | dispatch(toggle()); 54 | }; 55 | 56 | if (loading) { 57 | return ; 58 | } 59 | 60 | return ( 61 | 62 | 63 | 64 |
65 | }> 66 | 67 | } /> 68 | } /> 69 | } /> 70 | } /> 71 | } /> 72 | } /> 73 | } 76 | /> 77 | } /> 78 | } /> 79 | } /> 80 | } 83 | /> 84 | } /> 85 | } /> 86 | } /> 87 | } /> 88 | } /> 89 | } /> 90 | } /> 91 | 92 | 93 |
94 |