├── .github
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── auto-label.yml
└── detect-duplicate-issues.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── backend
├── .env.example
├── .gitignore
├── Makefile
├── Routes
│ ├── Router.js
│ ├── eventRoutes.js
│ ├── noteRoutes.js
│ ├── uploads
│ │ ├── profilePhoto-1729164520517-905517046.jpg
│ │ ├── profilePhoto-1729164538736-40559474.jpg
│ │ └── profilePhoto-1729164567074-811265604.jpg
│ └── user.routes.js
├── controllers
│ ├── noteControllers.js
│ └── user.controllers.js
├── index.js
├── mail
│ ├── contactUsMailSender.js
│ ├── forgotPAsswordOtpMail.js
│ └── sendMail.js
├── middlewares
│ └── user.middlewares.js
├── models
│ ├── contactModel.js
│ ├── eventModel.js
│ ├── feedbackModel.js
│ ├── noteModel.js
│ └── userModel.js
├── package-lock.json
├── package.json
├── uploads
│ ├── profilePhoto-1721713869259-990478442.png
│ ├── profilePhoto-1721713968989-129215121.jpeg
│ └── profilePhoto-1721714084138-572334207.jpg
├── utilities.js
├── utils
│ ├── const.js
│ └── multer.js
└── vercel.json
├── frontend
├── .env.development
├── .env.example
├── .eslintrc.cjs
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
│ ├── logo.png
│ └── testimonial-section.png
├── src
│ ├── App.css
│ ├── App.jsx
│ ├── assets
│ │ └── images
│ │ │ ├── add-notes.svg
│ │ │ ├── addPost.svg
│ │ │ ├── cross.svg
│ │ │ ├── hero1.jpg
│ │ │ ├── hero2.png
│ │ │ ├── hero3.png
│ │ │ ├── logo
│ │ │ ├── dark-logo.jpg
│ │ │ └── whiteLogo.jpg
│ │ │ ├── no-data.svg
│ │ │ └── noFound.svg
│ ├── components
│ │ ├── About
│ │ │ └── About.jsx
│ │ ├── ArchivedNotes
│ │ │ └── ArchivedNotes.jsx
│ │ ├── Brands.jsx
│ │ ├── Calendar
│ │ │ └── Calendar.jsx
│ │ ├── Cards
│ │ │ ├── NoteCard.jsx
│ │ │ └── ProfileInfo.jsx
│ │ ├── CircularLoader.jsx
│ │ ├── Contact
│ │ │ └── Contact.jsx
│ │ ├── Contributors
│ │ │ ├── Contributors.css
│ │ │ └── Contributors.jsx
│ │ ├── EmptyCard
│ │ │ └── EmptyCard.jsx
│ │ ├── ErrorPage.jsx
│ │ ├── Faq.jsx
│ │ ├── FilterTags.jsx
│ │ ├── Footer.jsx
│ │ ├── Footer
│ │ │ └── logo.png
│ │ ├── GoogleTranslate.jsx
│ │ ├── Hero
│ │ │ ├── Hero.jsx
│ │ │ └── styles.css
│ │ ├── Input
│ │ │ ├── AddAttachmentInput.jsx
│ │ │ ├── PasswordInput.jsx
│ │ │ └── TagInput.jsx
│ │ ├── Loading.jsx
│ │ ├── Modal.jsx
│ │ ├── Navbar-2.jsx
│ │ ├── Navbar.jsx
│ │ ├── Preloader.jsx
│ │ ├── Pricing.jsx
│ │ ├── Progressbar.jsx
│ │ ├── Scroll-on-top
│ │ │ └── ScrollOnTop.jsx
│ │ ├── SearchBar
│ │ │ └── SearchBar.jsx
│ │ ├── Tabs.jsx
│ │ ├── Testimonial.jsx
│ │ ├── Toggle.jsx
│ │ └── sticky_footer
│ │ │ ├── Content.jsx
│ │ │ └── Footer.jsx
│ ├── hooks
│ │ └── noteActions.jsx
│ ├── index.css
│ ├── main.jsx
│ ├── pages
│ │ ├── ForgotPassword
│ │ │ ├── NewPassword.jsx
│ │ │ ├── VerifyEmail.jsx
│ │ │ └── VerifyOtp.jsx
│ │ ├── Home
│ │ │ ├── AddEditNotes.jsx
│ │ │ ├── Home.jsx
│ │ │ └── Templates.jsx
│ │ ├── Login
│ │ │ └── Login.jsx
│ │ ├── ProfilePage
│ │ │ └── ProfilePage.jsx
│ │ └── Signup
│ │ │ └── Signup.jsx
│ ├── setupProxy.js
│ └── utils
│ │ ├── ProtectedRoute.jsx
│ │ ├── axiosInstance.js
│ │ └── helper.js
├── tailwind.config.js
└── vite.config.js
├── package-lock.json
├── package.json
└── todo
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Issue Template
2 |
3 | ## Description
4 |
5 | A clear and concise description of what the issue is. Include any relevant information such as version, environment, or conditions where the issue was observed.
6 |
7 | ## Steps to Reproduce
8 |
9 | Please provide step-by-step instructions on how to reproduce the issue:
10 |
11 | 1. Go to '...'
12 | 2. Click on '...'
13 | 3. Scroll down to '...'
14 | 4. See error
15 |
16 | ## Expected Behavior
17 |
18 | Describe what you expected to happen.
19 |
20 | ## Actual Behavior
21 |
22 | Describe what actually happens when you follow the steps.
23 |
24 | ## Screenshots
25 |
26 | If applicable, add screenshots to help explain the issue.
27 |
28 | ## Additional Context
29 |
30 | Add any other context or information about the issue here.
31 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | Please include a summary of the change and which issue is fixed. Also include relevant motivation and context.
4 |
5 | - Fixes #(issue number)
6 |
7 | ## Type of change
8 |
9 | Please delete options that are not relevant.
10 |
11 | - [ ] Bug fix (non-breaking change which fixes an issue)
12 | - [ ] New feature (non-breaking change which adds functionality)
13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
14 | - [ ] Documentation update
15 | - [ ] Tests update
16 |
17 | ## Checklist
18 |
19 | - [ ] My code follows the style guidelines of this project
20 | - [ ] I have performed a self-review of my own code
21 | - [ ] I have commented my code, particularly in hard-to-understand areas
22 | - [ ] My changes generate no new warnings
23 | - [ ] New and existing unit tests pass locally with my changes
24 | - [ ] I have maintained a clean commit history by using the necessary Git commands
25 | - [ ] I have checked that my code does not cause any merge conflicts
26 |
27 | ## Screenshots (if applicable)
28 |
29 | Add screenshots to help explain the changes (if necessary).
--------------------------------------------------------------------------------
/.github/auto-label.yml:
--------------------------------------------------------------------------------
1 | name: Auto Label Issue/PR
2 |
3 | on:
4 | issues:
5 | types: [opened, reopened, edited]
6 | pull_request:
7 | types: [opened, reopened, edited]
8 |
9 | jobs:
10 | label_issue_pr:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | issues: write
14 | pull-requests: write
15 | steps:
16 | - name: Label Issue/PR
17 | uses: actions/github-script@v6
18 | with:
19 | github-token: ${{secrets.GITHUB_TOKEN}}
20 | script: |
21 | const isIssue = context.payload.issue !== undefined;
22 | const item = isIssue ? context.payload.issue : context.payload.pull_request;
23 | const itemBody = item.body ? item.body.toLowerCase() : '';
24 | const itemTitle = item.title.toLowerCase();
25 |
26 | // Add labels to both issues and pull requests
27 | await github.rest.issues.addLabels({
28 | owner: context.repo.owner,
29 | repo: context.repo.repo,
30 | issue_number: item.number,
31 | labels: ['gssoc-ext', 'hacktoberfest-accepted']
32 | });
33 |
34 | const addLabel = async (label) => {
35 | await github.rest.issues.addLabels({
36 | owner: context.repo.owner,
37 | repo: context.repo.repo,
38 | issue_number: item.number,
39 | labels: [label]
40 | });
41 | };
42 |
--------------------------------------------------------------------------------
/.github/detect-duplicate-issues.yml:
--------------------------------------------------------------------------------
1 | name: Duplicate Issue Detector
2 |
3 | on:
4 | issues:
5 | types: [opened]
6 |
7 | jobs:
8 | detect-duplicate:
9 | runs-on: ubuntu-latest
10 | steps:
11 |
12 | - name: Check out repository
13 | uses: actions/checkout@v2
14 |
15 | - name: Detect duplicate issues
16 | id: detect
17 | uses: actions-cool/issues-similarity-analysis@v1
18 | with:
19 | token: ${{ secrets.GITHUB_TOKEN }}
20 | owner: ${{ github.repository_owner }}
21 | repo: ${{ github.event.repository.name }}
22 | threshold: 0.7 # Similarity threshold; adjust as needed
23 | label: duplicate
24 | close: true
25 |
26 | - name: Comment on Duplicate Issue
27 | if: steps.detect.outputs.duplicate == 'true'
28 | run: |
29 | curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
30 | -H "Accept: application/vnd.github.v3+json" \
31 | https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments \
32 | -d '{"body": "This issue is a duplicate of #${{ steps.detect.outputs.original_issue_number }}. Please refer to the original issue for updates."}'
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /frontend/.env
2 | /backend/.env
3 | todo
4 | node_modules
5 | /frontend/node_modules
6 | /backend/node_modules
7 | .env
8 | .env.development
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct 🫱🏻🫲🏻
2 |
3 | ## 1. Purpose
4 | At Scribbie, we aim to create an open and welcoming environment where contributors from all backgrounds feel valued and respected. This code of conduct outlines expectations for behavior to ensure everyone can contribute in a harassment-free environment.
5 |
6 | ## 2. Scope
7 | This Code of Conduct applies to all contributors, including maintainers and users. It applies to both project spaces and public spaces where an individual represents the project.
8 |
9 | ## 3. Our Standards
10 | In a welcoming environment, contributors should:
11 |
12 | Be kind and respectful to others.
13 | Collaborate with others in a constructive manner.
14 | Provide feedback in a respectful and considerate way.
15 | Be open to differing viewpoints and experiences.
16 | Show empathy and understanding towards others.
17 | Unacceptable behaviors include, but are not limited to:
18 |
19 | Use of derogatory comments, insults, or personal attacks.
20 | Harassment of any kind, including but not limited to: offensive comments related to gender, race, religion, or any other personal characteristics.
21 | The publication of private information without consent.
22 | Any behavior that could be perceived as discriminatory, intimidating, or threatening.
23 |
24 | ## 4. Enforcement
25 | Instances of unacceptable behavior may be reported to the project team. All complaints will be reviewed and investigated and will result in appropriate action.
26 |
27 | ## 5. Acknowledgment
28 | By contributing to Scribbie, you agree to adhere to this Code of Conduct and help create a safe, productive, and inclusive environment for all.
29 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Scribbie 🎉
2 |
3 | Thank you for considering contributing to Scribbie! We welcome all contributions, whether you're fixing bugs, improving documentation, or adding new features. To help you get started, we've outlined the process for contributing to the project.
4 |
5 |
6 |
7 | # Code of Conduct 📃
8 |
9 | Please read and follow our [Code of Conduct.](https://github.com/Scribbie-Notes/notes-app/blob/main/CODE_OF_CONDUCT.md)
10 |
11 |
12 |
13 | #
Star our Repository ⭐
14 |
15 | ### [](https://github.com/Scribbie-Notes/notes-app/stargazers) [](https://github.com/Scribbie-Notes/notes-app/network/members) [](https://github.com/Scribbie-Notes/notes-app/issues) [](https://github.com/Scribbie-Notes/notes-app/pulls) [](https://github.com/Scribbie-Notes/notes-app/pulls?q=is%3Apr+is%3Aclosed)
16 |
17 |
18 |
19 | # Need Help With The Basics? 🤔
20 |
21 | If you're new to Git and GitHub, no worries! Here are some useful resources:
22 |
23 | - [Forking a Repository](https://help.github.com/en/github/getting-started-with-github/fork-a-repo)
24 | - [Cloning a Repository](https://help.github.com/en/desktop/contributing-to-projects/creating-an-issue-or-pull-request)
25 | - [How to Create a Pull Request](https://opensource.com/article/19/7/create-pull-request-github)
26 | - [Getting Started with Git and GitHub](https://towardsdatascience.com/getting-started-with-git-and-github-6fcd0f2d4ac6)
27 | - [Learn GitHub from Scratch](https://docs.github.com/en/get-started/start-your-journey/git-and-github-learning-resources)
28 |
29 |
30 |
31 | # Project Structure 📂
32 |
33 | ```bash
34 | NOTES-APP/
35 | ├── .github/ # GitHub-related configurations such as workflows, issue templates, etc
36 | │
37 | ├── backend/ # All the backend development files included here
38 | │
39 | ├── frontend/ # All the frontend development files included here
40 | │
41 | ├── .gitignore
42 | │
43 | ├── CODE_OF_CONDUCT.md # Some rules for the contributors
44 | │
45 | ├── CONTRIBUTING.md # Instructions for the contributors
46 | │
47 | ├── package-lock.json
48 | │
49 | ├── package.json
50 | │
51 | ├── README.md # Some instructions about contributions
52 | │
53 | ├── todo
54 | ```
55 |
56 |
57 |
58 | # Contribution Guidelines 📚
59 |
60 | - Follow the coding standards and guidelines.
61 | - Write meaningful commit messages.
62 | - Ensure your changes do not introduce breaking bugs.
63 | - Be responsive to feedback and comments from the project maintainers.
64 | - Respect deadlines and guidelines in case you're working as part of a program (e.g., GSSoC).
65 |
66 | We appreciate your contributions and look forward to collaborating with you to make Scribbie a better tool!
67 |
68 |
69 |
70 | # First Pull Request ✨
71 |
72 | 1. **Star this repository**
73 | Click on the top right corner marked as **Stars** at last.
74 |
75 | 2. **Fork this repository**
76 | Click on the top right corner marked as **Fork** at second last.
77 |
78 | 3. **Clone the forked repository**
79 |
80 | ```bash
81 | git clone https://github.com//notes-app.git
82 | ```
83 |
84 | 4. **Navigate to the project directory**
85 |
86 | ```bash
87 | cd notes-app
88 | ```
89 |
90 | 5. **Create a new branch**
91 |
92 | ```bash
93 | git checkout -b
94 | ```
95 |
96 | 6. **To make changes**
97 |
98 | ```bash
99 | git add .
100 | ```
101 |
102 | 7. **Now to commit**
103 |
104 | ```bash
105 | git commit -m "add comment according to your changes or addition of features inside this"
106 | ```
107 |
108 | 8. **Push your local commits to the remote repository**
109 |
110 | ```bash
111 | git push -u origin
112 | ```
113 |
114 | 9. **Create a Pull Request**
115 |
116 | 10. **Congratulations! 🎉 you've made your contribution**
117 |
118 |
119 |
120 | # Alternatively, contribute using GitHub Desktop 🖥️
121 |
122 | 1. **Open GitHub Desktop:**
123 | Launch GitHub Desktop and log in to your GitHub account if you haven't already.
124 |
125 | 2. **Clone the Repository:**
126 | - If you haven't cloned the project repository yet, you can do so by clicking on the "File" menu and selecting "Clone Repository."
127 | - Choose the project repository from the list of repositories on GitHub and clone it to your local machine.
128 |
129 | 3.**Switch to the Correct Branch:**
130 | - Ensure you are on the branch that you want to submit a pull request for.
131 | - If you need to switch branches, you can do so by clicking on the "Current Branch" dropdown menu and selecting the desired branch.
132 |
133 | 4. **Make Changes:**
134 | - Make your changes to the code or files in the repository using your preferred code editor.
135 |
136 | 5. **Commit Changes:**
137 | - In GitHub Desktop, you'll see a list of the files you've changed. Check the box next to each file you want to include in the commit.
138 | - Enter a summary and description for your changes in the "Summary" and "Description" fields, respectively. Click the "Commit to " button to commit your changes to the local branch.
139 |
140 | 6. **Push Changes to GitHub:**
141 | - After committing your changes, click the "Push origin" button in the top right corner of GitHub Desktop to push your changes to your forked repository on GitHub.
142 |
143 | 7. **Create a Pull Request:**
144 | - Go to the GitHub website and navigate to your fork of the project repository.
145 | - You should see a button to "Compare & pull request" between your fork and the original repository. Click on it.
146 |
147 | 8. **Review and Submit:**
148 | - On the pull request page, review your changes and add any additional information, such as a title and description, that you want to include with your pull request.
149 | - Once you're satisfied, click the "Create pull request" button to submit your pull request.
150 |
151 | 9. **Wait for Review:**
152 | Your pull request will now be available for review by the project maintainers. They may provide feedback or ask for changes before merging your pull request into the main branch of the project repository.
153 |
154 |
155 |
156 | # Good Coding Practices 🧑💻
157 |
158 | 1. **Follow the Project's Code Style**
159 |
160 | - Maintain consistency with the existing code style (indentation, spacing, comments).
161 | - Use meaningful and descriptive names for variables, functions, and classes.
162 | - Keep functions short and focused on a single task.
163 | - Avoid hardcoding values; instead, use constants or configuration files when possible.
164 |
165 | 2. **Write Clear and Concise Comments**
166 |
167 | - Use comments to explain why you did something, not just what you did.
168 | - Avoid unnecessary comments that state the obvious.
169 | - Document complex logic and functions with brief explanations to help others understand your thought -process.
170 |
171 | 3. **Keep Code DRY (Don't Repeat Yourself)**
172 |
173 | - Avoid duplicating code. Reuse functions, methods, and components whenever possible.
174 | - If you find yourself copying and pasting code, consider creating a new function or component.
175 |
176 | 4. **Write Tests**
177 |
178 | - Write unit tests for your functions and components.
179 | - Ensure your tests cover both expected outcomes and edge cases.
180 | - Run tests locally before making a pull request to make sure your changes don’t introduce new bugs.
181 |
182 | 5. **Code Reviews and Feedback**
183 |
184 | - Be open to receiving constructive feedback from other contributors.
185 | - Conduct code reviews for others and provide meaningful suggestions to improve the code.
186 | - Always refactor your code based on feedback to meet the project's standards.
187 |
188 |
189 |
190 | # Pull Request Process 🚀
191 |
192 | When submitting a pull request, please adhere to the following:
193 |
194 | 1. **Self-review your code** before submission. 😀
195 | 2. Include a detailed description of the functionality you’ve added or modified.
196 | 3. Comment your code, especially in complex sections, to aid understanding.
197 | 4. Add relevant screenshots to assist in the review process.
198 | 5. Submit your PR using the provided template and hang tight; we'll review it as soon as possible! 🚀
199 |
200 |
201 |
202 | # Issue Report Process 📌
203 |
204 | To report an issue, follow these steps:
205 |
206 | 1. Navigate to the project's issues section :- [Issues](https://github.com/Scribbie-Notes/notes-app/issues/new)
207 | 2. Please kindly choose the appropriate template according to your issue.
208 | 3. Provide a clear and concise description of the issue.
209 | 4. Wait until someone looks into your report.
210 | 5. Begin working on the issue only after you have been assigned to it. 🚀
211 |
212 |
213 |
214 | # Thank you for contributing 💗
215 |
216 | We truly appreciate your time and effort to help improve our project. Feel free to reach out if you have any questions or need guidance. Happy coding! 🚀
217 |
218 | ##
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Scribbie Notes
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 | # **📚 Scribbie Notes App 📚**
2 |
3 |
4 |
5 | **Scribbie** is a powerful, intuitive note-taking website built specifically for working professionals who want to manage their notes seamlessly and effectively.
6 |
7 |
8 |
9 | 🌟 Stars
10 | 🍴 Forks
11 | 🐛 Issues
12 | 🔔 Open PRs
13 | 🔕 Close PRs
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ---
28 |
29 | ## 🔥 Tech Stack
30 |
31 | - **Frontend**: React.js, TailwindCSS, React Hot Toast
32 | - **Backend**: Node.js, Express.js, MongoDB
33 | - **Authentication**: Google Auth
34 | - **Deployment**: Vercel
35 |
36 | ---
37 |
38 | ## 💻 Getting Started
39 |
40 | ### Prerequisites
41 |
42 | Before you begin, ensure that you have the following installed:
43 |
44 | 1. **Node.js**: [Download Node.js](https://nodejs.org)
45 | 2. **MongoDB**: [Download MongoDB](https://www.mongodb.com)
46 | 3. **Git**: [Download Git](https://git-scm.com)
47 |
48 | ### 🚀 Running Scribbie on Your Local Machine
49 |
50 | Follow these steps to get Scribbie running on your local machine:
51 |
52 | 1. **Clone the repository**:
53 | ```bash
54 | git clone https://github.com/yashmandi/notes-app.git
55 | ```
56 |
57 | 2. **Navigate to the project directory**:
58 | ```bash
59 | cd notes-app
60 | ```
61 |
62 | 3. **Install dependencies for both backend and frontend**:
63 |
64 | - Install backend dependencies:
65 | ```bash
66 | cd backend
67 | npm install
68 | ```
69 |
70 | - Install frontend dependencies:
71 | ```bash
72 | cd ../frontend
73 | npm install
74 | ```
75 |
76 | 4. **Set up environment variables**: Create `.env` files in both backend and frontend directories with the necessary values.
77 | Here's a sample configuration:
78 |
79 | **Backend `.env`:**
80 | ```bash
81 | # Backend Environment Variables
82 | PORT=5000
83 | MONGODB_URI=mongodb://localhost:27017/your-database-name
84 | GOOGLE_API_TOKEN=your_google_api_token_here
85 | ```
86 |
87 | **Frontend `.env`:**
88 | ```bash
89 | # Frontend Environment Variables
90 | VITE_BACKEND_URL=http://localhost:5000
91 | VITE_REACT_APP_GOOGLE_API_TOKEN=your_google_api_token_here
92 | ```
93 |
94 | 5. **Run the project**:
95 | - Run the backend server:
96 | ```bash
97 | cd backend
98 | npm start
99 | ```
100 | - Run the frontend:
101 | ```bash
102 | cd ../frontend
103 | npm run dev
104 | ```
105 |
106 | ---
107 |
108 | # 📚 Contribution Guidelines
109 | We welcome contributions from the community! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for detailed guidelines.
110 |
111 | ---
112 |
113 |
114 | # 👀 Our Contributors
115 |
116 | - We extend our heartfelt gratitude for your invaluable contribution to our project! Your efforts play a pivotal role in elevating Ratna-Supermarket to greater heights.
117 | - Make sure you show some love by giving ⭐ to our repository.
118 |
119 |
125 |
126 | ---
127 |
128 | # 🤝 Code of Conduct
129 | All contributors must adhere to our [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) to ensure a positive collaboration environment.
130 |
131 | ---
132 |
133 | 🎉 Happy Contributing!
134 | Let’s work together to make Scribbie an even better tool!
135 |
136 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | Security Policy
2 | Supported Versions
3 | The following versions of Notes-App are currently supported with security updates:
4 |
5 | Version Supported
6 | 1.x.x ✅ Supported
7 | 0.x.x ❌ Not supported
8 | Reporting a Vulnerability
9 | If you discover a security vulnerability in Notes-App, we encourage you to report it as soon as possible. We will investigate all legitimate reports and do our best to quickly fix the issue.
10 |
11 | # How to Report
12 | Please report vulnerabilities by emailing us at notesapp@gmail.com. Include as much detail as possible to help us identify and fix the issue swiftly.
13 | Do not share the vulnerability publicly until it has been addressed and a patch is available.
14 | Security Updates
15 | We will notify users via GitHub releases for any critical security updates.
16 | Minor security patches will be included in regular updates as needed.
17 |
18 | # Security Best Practices
19 | Make sure to use the latest version of Notes-App for the latest security features and patches.
20 | Follow password best practices, such as using strong, unique passwords for each account.
21 | Regularly update your dependencies to the latest versions.
22 |
23 | # Acknowledgements
24 | We appreciate contributions from the community and researchers who help us improve the security of Notes-App. Thank you for keeping the platform secure for everyone!
25 |
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | # BREVO_USER=""
2 | # BREVO_PASSWORD=""
3 | # BREVO_HOST="smtp-relay.brevo.com"
4 | # BREVO_PORT=587
5 |
6 |
7 | # MONGO_URI=""
8 | # PORT=3000
9 | # ACCESS_TOKEN_SECRET=""
10 | # GOOGLE_API_TOKEN=""
11 |
12 |
13 | PORT=5000
14 | MONGODB_URI=ADD_YOUR_MONGODB_URI_HERE
15 | GOOGLE_API_TOKEN=your_google_api_token_here
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | .env.*
2 | .env
3 | !.env.example
4 |
5 | node_modules
--------------------------------------------------------------------------------
/backend/Makefile:
--------------------------------------------------------------------------------
1 | all:
2 | npm start &
3 | cd ../frontend && npm run dev
4 |
--------------------------------------------------------------------------------
/backend/Routes/eventRoutes.js:
--------------------------------------------------------------------------------
1 | // routes/event.js
2 | import { Router } from "express";
3 | import Event from "../models/eventModel.js";
4 | import { authenticationToken } from "../middlewares/user.middlewares.js";
5 |
6 | const eventRoutes = Router();
7 |
8 | // Middleware to extract user from request
9 | const getUserFromRequest = (req) => req.user?.user;
10 |
11 | // Create an event
12 | eventRoutes.post('/add/event', authenticationToken, async (req, res) => {
13 | const { date, title, color, description } = req.body;
14 | const user = getUserFromRequest(req);
15 |
16 | if (!user) {
17 | return res.status(401).json({ message: 'Unauthorized' });
18 | }
19 |
20 | try {
21 | const event = new Event({ date, title, color, description, userId: user._id });
22 | await event.save();
23 | res.status(201).json(event);
24 | } catch (error) {
25 | res.status(400).json({ message: error.message });
26 | }
27 | });
28 |
29 | // Get all events
30 | eventRoutes.get('/get/events', authenticationToken, async (req, res) => {
31 | const user = getUserFromRequest(req);
32 |
33 | try {
34 | const events = await Event.find({ userId: user._id });
35 | res.json(events);
36 | } catch (error) {
37 | res.status(500).json({ message: error.message });
38 | }
39 | });
40 |
41 | // Delete an event
42 | eventRoutes.delete('/delete/event/:id', authenticationToken, async (req, res) => {
43 | try {
44 | const event = await Event.findByIdAndDelete(req.params.id);
45 | if (!event) {
46 | return res.status(404).json({ message: 'Event not found' });
47 | }
48 | res.json({ message: 'Event deleted' });
49 | } catch (error) {
50 | res.status(500).json({ message: error.message });
51 | }
52 | });
53 |
54 | export default eventRoutes;
55 |
--------------------------------------------------------------------------------
/backend/Routes/noteRoutes.js:
--------------------------------------------------------------------------------
1 | import multer from "multer";
2 | import { Router } from "express";
3 | import { storage } from "../utils/multer.js";
4 | import { HTTP_STATUS, MESSAGES, ERROR_MESSAGES } from "../utils/const.js";
5 |
6 | import { authenticationToken } from "../middlewares/user.middlewares.js";
7 | import {
8 | addNoteController,
9 | archiveNoteController,
10 | bulkUpdateNotePinnedController,
11 | deleteMultipleNotesController,
12 | deleteNoteByIdcontroller,
13 | editNoteByIdController,
14 | getAllNotesController,
15 | getArchiveNotesController,
16 | searchNotesController,
17 | updateNotePinnedController,
18 | unArchiveController,
19 | updateNotesBackgroundController,
20 | viewNotesController,
21 | restoreDeletedNotesController
22 | } from "../controllers/noteControllers.js";
23 | import noteModel from "../models/noteModel.js";
24 | import mongoose from "mongoose";
25 |
26 | const noteRoutes = Router();
27 |
28 | // const upload = multer({ storage: storage });
29 | //upload multiple attachments files
30 | const uploadMultiple = multer({ storage: storage }).array("attachments", 10);
31 |
32 | noteRoutes.post(
33 | "/add-note",
34 | authenticationToken,
35 | uploadMultiple,
36 | addNoteController
37 | );
38 |
39 |
40 | noteRoutes.get("/view-note/:noteId", viewNotesController);
41 |
42 |
43 | // Configure multer to not accept any files
44 | const upload_note = multer().none(); // This allows only non-file data
45 |
46 | noteRoutes.put(
47 | "/edit-note/:noteId",
48 | authenticationToken, // Ensure this middleware runs first
49 | upload_note, // Use the updated multer configuration
50 | editNoteByIdController // Your controller function
51 | );
52 |
53 | noteRoutes.put("/update-notes-background", authenticationToken,updateNotesBackgroundController);
54 |
55 | noteRoutes.get("/get-all-notes", authenticationToken, getAllNotesController);
56 |
57 | noteRoutes.get("/get-archived-notes", authenticationToken,getArchiveNotesController);
58 |
59 | noteRoutes.delete("/delete-note/:noteId", authenticationToken, deleteNoteByIdcontroller);
60 |
61 | noteRoutes.delete("/delete-multiple-notes", authenticationToken, deleteMultipleNotesController);
62 |
63 | noteRoutes.put(
64 | "/update-note-pinned/:noteId",
65 | authenticationToken,
66 | updateNotePinnedController
67 | );
68 |
69 | noteRoutes.put('/bulk-update-notes-pinned', bulkUpdateNotePinnedController);
70 |
71 | noteRoutes.put('/archive-notes',archiveNoteController);
72 | noteRoutes.put('/un-archive-notes',unArchiveController);
73 | noteRoutes.get("/search-notes/", authenticationToken, searchNotesController);
74 | noteRoutes.put("/undo-delete-notes", authenticationToken, async (req, res) => {
75 | try {
76 | const { noteIds } = req.body;
77 | // Change this line - req.user might be structured differently
78 | const { user } = req.user; // or however your user ID is structured
79 | console.log("noteIds:", noteIds);
80 | if (!noteIds || !Array.isArray(noteIds) || noteIds.length === 0) {
81 | return res.status(HTTP_STATUS.BAD_REQUEST).json({
82 | error: true,
83 | message: ERROR_MESSAGES.PROVIDE_FIELD_TO_UPDATE,
84 | });
85 | }
86 |
87 | // Restore notes by setting `deleted` back to false
88 | const result = await noteModel.updateMany(
89 | { _id: { $in: noteIds }, userId: user._id },
90 | { $set: { deleted: false, deletedAt: null } }
91 | )
92 |
93 | console.log("Update result:", result);
94 |
95 | if (result.modifiedCount === 0) {
96 | return res.status(HTTP_STATUS.NOT_FOUND).json({
97 | error: true,
98 | message: ERROR_MESSAGES.NOTES_NOT_FOUND,
99 | });
100 | }
101 |
102 | return res.json({
103 | error: false,
104 | message: MESSAGES.NOTES_RESTORED_SUCCESSFULLY,
105 | modifiedCount: result.modifiedCount,
106 | });
107 | } catch (error) {
108 | console.error("Error restoring notes: ", error);
109 | return res
110 | .status(HTTP_STATUS.INTERNAL_SERVER_ERROR)
111 | .json({ error: true, message: ERROR_MESSAGES.INTERNAL_SERVER_ERROR });
112 | }
113 | });
114 |
115 |
116 | export default noteRoutes;
117 |
--------------------------------------------------------------------------------
/backend/Routes/uploads/profilePhoto-1729164520517-905517046.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/backend/Routes/uploads/profilePhoto-1729164520517-905517046.jpg
--------------------------------------------------------------------------------
/backend/Routes/uploads/profilePhoto-1729164538736-40559474.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/backend/Routes/uploads/profilePhoto-1729164538736-40559474.jpg
--------------------------------------------------------------------------------
/backend/Routes/uploads/profilePhoto-1729164567074-811265604.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/backend/Routes/uploads/profilePhoto-1729164567074-811265604.jpg
--------------------------------------------------------------------------------
/backend/Routes/user.routes.js:
--------------------------------------------------------------------------------
1 | import { Router } from "express";
2 | import multer from "multer";
3 | import { storage } from "../utils/multer.js";
4 |
5 | import {
6 | contactUsMailController,
7 | createAccountController,
8 | deleteUserController,
9 | feedbackSubmitController,
10 | getCurrentAccountController,
11 | googleAuthController,
12 | loginAccountController,
13 | updateEmailController,
14 | updateFullNameController,
15 | updatePhoneController,
16 | updateProfilePhotoController,
17 | verifyAccountController,
18 | } from "../controllers/user.controllers.js";
19 |
20 | import {
21 | authenticationToken,
22 | createAccountMiddleware,
23 | } from "../middlewares/user.middlewares.js";
24 |
25 | const upload = multer({ storage: storage });
26 | //upload multiple attachments files
27 | // const uploadMultiple = multer({ storage: storage }).array("attachments", 10);
28 |
29 | const userRoutes = Router();
30 |
31 | userRoutes.post(
32 | "/create-account",
33 | createAccountMiddleware,
34 | createAccountController
35 | );
36 |
37 | userRoutes.get("/verify/:token", verifyAccountController);
38 |
39 | // Login
40 | userRoutes.post("/login", loginAccountController);
41 |
42 | userRoutes.get("/get-user", authenticationToken, getCurrentAccountController);
43 |
44 | userRoutes.delete("/delete-user", authenticationToken, deleteUserController);
45 |
46 | userRoutes.put(
47 | "/update-fullName",
48 | authenticationToken,
49 | updateFullNameController
50 | );
51 |
52 | userRoutes.put("/update-email", authenticationToken, updateEmailController);
53 |
54 | userRoutes.put("/update-phone", updatePhoneController);
55 |
56 | userRoutes.put(
57 | "/update-profile-photo",
58 | upload.single("profilePhoto"),
59 | updateProfilePhotoController
60 | );
61 |
62 | userRoutes.post("/google-auth", googleAuthController);
63 |
64 | userRoutes.post("/submit", feedbackSubmitController);
65 |
66 | userRoutes.post("/contact", contactUsMailController);
67 |
68 | export default userRoutes;
69 |
--------------------------------------------------------------------------------
/backend/index.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import mongoose from "mongoose";
3 | import dotenv from "dotenv";
4 | import path from "path";
5 | import { fileURLToPath } from "url";
6 | import cors from "cors";
7 | import userRoutes from "./Routes/user.routes.js";
8 | import noteRoutes from "./Routes/noteRoutes.js";
9 | import eventRoutes from "./Routes/eventRoutes.js";
10 |
11 |
12 | // Defined __filename and __dirname
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = path.dirname(__filename);
15 |
16 | // const Router = require("./Routes/Router");
17 |
18 | const app = express();
19 |
20 | app.use(express.json());
21 |
22 | // Determine which .env file to use based on the NODE_ENV
23 | const envPath =
24 | process.env.NODE_ENV === "production" ? ".env.production" : ".env";
25 | dotenv.config({ path: path.resolve(__dirname, envPath) });
26 |
27 | const { MONGO_URI } = process.env;
28 | console.log(MONGO_URI);
29 |
30 | // Log the MongoDB URI to verify it's loaded
31 | console.log("MongoDB URI:", MONGO_URI);
32 |
33 | // Check if MONGO_URI is defined
34 | if (!MONGO_URI) {
35 | console.error("MONGO_URI is not defined in environment variables.");
36 | process.exit(1); // Exit the application if MONGO_URI is not set
37 | }
38 |
39 | // Use cors middleware before defining any routes
40 | const allowedOrigins =
41 | process.env.NODE_ENV === "production"
42 | ? ["https://scribbie-notes.vercel.app"]
43 | : ["http://localhost:5173"];
44 |
45 | app.use(
46 | cors({
47 | origin: allowedOrigins,
48 | credentials: true, // access-control-allow-credentials:true
49 | methods: "GET,PUT,POST,DELETE",
50 | optionSuccessStatus: 200,
51 | })
52 | );
53 |
54 | // Connect to MongoDB
55 | (async function () {
56 | try {
57 | await mongoose.connect(MONGO_URI)
58 | console.log("MongoDB connected")
59 | } catch (error) {
60 | console.error("MongoDB connection error:", error);
61 | }
62 | })();
63 |
64 | //new better and structured routes
65 | app.use(userRoutes);
66 | app.use(noteRoutes);
67 | app.use(eventRoutes);
68 |
69 | const PORT = process.env.PORT || 5000;
70 | app.listen(PORT, () => {
71 | console.log(`Server is running on http://localhost:${PORT}`);
72 | });
73 |
74 |
75 | export default app;
--------------------------------------------------------------------------------
/backend/mail/contactUsMailSender.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer'
2 | import asyncHandler from 'express-async-handler'
3 | import dotenv from 'dotenv'
4 | dotenv.config();
5 | const transporter = nodemailer.createTransport({
6 | host: process.env.BREVO_HOST,
7 | port: process.env.BREVO_PORT,
8 | secure: false,
9 | auth: {
10 | user: process.env.BREVO_USER,
11 | pass: process.env.BREVO_PASSWORD,
12 | },
13 | });
14 |
15 | const contactSendMail = asyncHandler(async (email, name, html) => {
16 | await transporter.sendMail({
17 | from: '"Notes App" ',
18 | to: email,
19 | name: name,
20 | html: html,
21 | });
22 | });
23 |
24 | export default contactSendMail;
--------------------------------------------------------------------------------
/backend/mail/forgotPAsswordOtpMail.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const nodemailer = require("nodemailer");
3 |
4 | // Create a Nodemailer transporter using SMTP
5 | const transporter = nodemailer.createTransport({
6 | service: "gmail", // or your preferred email service
7 | auth: {
8 | user: process.env.BREVO_USER,
9 | pass: process.env.BREVO_PASS,
10 | },
11 | });
12 |
13 | exports.sendVerificationMail = async(email, verificationCode) => {
14 | const emailText = `
15 | Dear Customer,
16 |
17 | Please use this verification code for resetting your password. Here's your code':
18 |
19 | Code: ${verificationCode}
20 |
21 | Thank you for choosing our service. We are happy to help you.
22 |
23 | Best regards,
24 | `;
25 | console.log("hii");
26 |
27 | try {
28 | await transporter.sendMail({
29 | from: '"Notes App" ',
30 | to: email,
31 | subject: "Password Reset Verification Code",
32 | text: emailText,
33 | });
34 |
35 | console.log("hlo");
36 |
37 |
38 | } catch (error) {
39 | console.log(`Failed to send verification email: ${error.message}`);
40 |
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/backend/mail/sendMail.js:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer';
2 | import asyncHandler from 'express-async-handler';
3 | import dotenv from 'dotenv'
4 | dotenv.config();
5 | const transporter = nodemailer.createTransport({
6 | host: process.env.BREVO_HOST,
7 | port: process.env.BREVO_PORT,
8 | secure: false,
9 | auth: {
10 | user: process.env.BREVO_USER,
11 | pass: process.env.BREVO_PASSWORD,
12 | },
13 | });
14 |
15 | const sendMail = asyncHandler(async (email, url) => {
16 | await transporter.sendMail({
17 | from: '"Notes App" ',
18 | to: email,
19 | subject: "Verification Link For Notes Account",
20 | text: "Click the link below to verify your account",
21 | html: url,
22 | });
23 | });
24 |
25 | export default sendMail;
--------------------------------------------------------------------------------
/backend/middlewares/user.middlewares.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import { HTTP_STATUS, ERROR_MESSAGES } from "../utils/const.js";
3 | const { ACCESS_TOKEN_SECRET, GOOGLE_API_TOKEN } = process.env;
4 |
5 |
6 | const createAccountMiddleware = async (req, res, next) => {
7 | const { fullName, email, password } = req.body;
8 |
9 | // fullname validations
10 | if (!fullName || fullName.trim() === "") {
11 | return res
12 | .status(HTTP_STATUS.BAD_REQUEST)
13 | .json({ error: true, message: ERROR_MESSAGES.NAME_REQUIRED });
14 | }
15 | const nameRegex = /^[A-Za-z]+(?: [A-Za-z]+)*$/;
16 | if (!nameRegex.test(fullName)) {
17 | return res
18 | .status(HTTP_STATUS.BAD_REQUEST)
19 | .json({ error: true, message: ERROR_MESSAGES.INVALID_NAME_FORMAT });
20 | }
21 |
22 | // email validations
23 | if (!email || email.trim() === "") {
24 | return res
25 | .status(HTTP_STATUS.BAD_REQUEST)
26 | .json({ error: true, message: ERROR_MESSAGES.EMAIL_REQUIRED });
27 | }
28 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
29 | if (!emailRegex.test(email)) {
30 | return res
31 | .status(HTTP_STATUS.BAD_REQUEST)
32 | .json({ error: true, message: ERROR_MESSAGES.INVALID_EMAIL_FORMAT });
33 | }
34 |
35 | // password validaitons
36 | if (!password || password.trim() === "") {
37 | return res
38 | .status(HTTP_STATUS.BAD_REQUEST)
39 | .json({ error: true, message: ERROR_MESSAGES.PASSWORD_REQUIRED });
40 | }
41 | if (!/[A-Z]/.test(password)) {
42 | return res.status(HTTP_STATUS.BAD_REQUEST).json({
43 | error: true,
44 | message: ERROR_MESSAGES.PASSWORD_UPPERCASE_REQUIRED,
45 | });
46 | }
47 | if (!/[a-z]/.test(password)) {
48 | return res.status(HTTP_STATUS.BAD_REQUEST).json({
49 | error: true,
50 | message: ERROR_MESSAGES.PASSWORD_LOWERCASE_REQUIRED,
51 | });
52 | }
53 | if (!/[!"#$%&'()*+,-.:;<=>?@[\]^_`{|}~]/.test(password)) {
54 | return res.status(HTTP_STATUS.BAD_REQUEST).json({
55 | error: true,
56 | message: ERROR_MESSAGES.PASSWORD_SPECIAL_CHAR_REQUIRED,
57 | });
58 | }
59 | if (!(password.length >= 8)) {
60 | return res
61 | .status(HTTP_STATUS.BAD_REQUEST)
62 | .json({ error: true, message: ERROR_MESSAGES.PASSWORD_MIN_LENGTH });
63 | }
64 |
65 | next();
66 | };
67 |
68 | const authenticationToken = (req, res, next) => {
69 | // console.log(req.headers)
70 | const token = req.headers["authorization"].split(" ")[1];
71 | // console.log("Authorization header:", token); // Log the token for debugging
72 |
73 | if (!token) {
74 | return res.status(403).json({ message: "No token provided." });
75 | }
76 |
77 | jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
78 | if (err) {
79 | console.error("Token verification error:", err); // Log the error
80 | return res.status(403).json({ message: "Token verification failed." });
81 | }
82 | req.user = user; // Attach user data to the request object
83 | next(); // Proceed to the next middleware/route handler
84 | });
85 | };
86 |
87 | export { createAccountMiddleware, authenticationToken };
88 |
--------------------------------------------------------------------------------
/backend/models/contactModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const contactSchema = new mongoose.Schema({
4 | first_name: {
5 | type: String,
6 | required: true,
7 | },
8 | last_name: {
9 | type: String,
10 | required: false, // Make this optional if you don't always collect it
11 | },
12 | user_email: {
13 | type: String,
14 | required: true,
15 | match: /.+\@.+\..+/ // Basic email format validation
16 | },
17 | message: {
18 | type: String,
19 | required: true,
20 | minlength: 1, // Minimum length of message
21 | },
22 | }, {
23 | timestamps: true, // Automatically adds createdAt and updatedAt fields
24 | });
25 |
26 | const Contact = mongoose.model('Contact', contactSchema);
27 |
28 | module.exports = Contact;
29 |
--------------------------------------------------------------------------------
/backend/models/eventModel.js:
--------------------------------------------------------------------------------
1 | // models/Event.js
2 | import mongoose from "mongoose";
3 |
4 | const eventSchema = new mongoose.Schema({
5 | date: {
6 | type: String,
7 | required: true,
8 | },
9 | title: {
10 | type: String,
11 | required: true,
12 | },
13 | color: {
14 | type: String,
15 | required: true,
16 | },
17 | description: {
18 | type: String,
19 | required: false,
20 | },
21 | userId: { type: String, required: true },
22 | });
23 |
24 | const Event = mongoose.model('Event', eventSchema);
25 | export default Event;
26 |
--------------------------------------------------------------------------------
/backend/models/feedbackModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const feedbackSchema = new Schema({
6 | name: { type: String, required: true },
7 | email: { type: String, required: true },
8 | feedback: { type: String, required: true },
9 | rating:{type:Number,required:true},
10 | createdOn: { type: Date, default: Date.now }
11 | });
12 |
13 | const Feedback = mongoose.model("Feedback", feedbackSchema);
14 | export default Feedback;
--------------------------------------------------------------------------------
/backend/models/noteModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const Schema = mongoose.Schema;
4 |
5 | const noteSchema = new Schema({
6 | title: { type: String, required: true },
7 | content: { type: String, required: true },
8 | tags: { type: [String], default: [] }, // Ensure this is an array
9 | attachments:{type:[String],default:[]},
10 | isPinned: { type: Boolean, required: false },
11 | isArchived:{type:Boolean , default:false},
12 | deleted: {
13 | type: Boolean,
14 | default: false, // Default is false, meaning the note is not deleted
15 | },
16 | userId: { type: String, required: true },
17 | createdOn: { type: Date, default: Date.now },
18 | background: { type: String },
19 | deletedAt: Date,
20 |
21 | });
22 |
23 |
24 | export default mongoose.model("Note", noteSchema);
25 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import bcrypt from "bcrypt"; // Import bcrypt
3 |
4 | const Schema = mongoose.Schema;
5 |
6 | const userSchema = new Schema({
7 | fullName: { type: String },
8 | email: { type: String },
9 | password: { type: String },
10 | phone: { type: String },
11 | createdOn: { type: Date, default: new Date().getTime() },
12 | profilePhoto: { type: String },
13 | verificationCode: {type: String, default: ""},
14 | isEmailVerified: { type: Boolean, default: false },
15 |
16 | });
17 | // hashing the password before saving it to the database by using PRE
18 | userSchema.pre("save", async function (next) {
19 | if (!this.isModified("password")) return next();
20 | //this will hash the password only the password field is bieng modified
21 | this.password = await bcrypt.hash(this.password, 10);
22 | next();
23 | });
24 | //creating a our own method for validating the password entered by the user
25 | userSchema.methods.checkPassword = async function (password) {
26 | return await bcrypt.compare(password, this.password);
27 | };
28 |
29 | export default mongoose.model("User", userSchema);
30 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notes-app-backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "nodemon index.js"
9 | },
10 | "type": "module",
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "@react-oauth/google": "^0.12.1",
15 | "axios": "^1.7.2",
16 | "bcrypt": "^5.1.1",
17 | "cors": "^2.8.5",
18 | "dotenv": "^16.4.5",
19 | "express": "^4.21.0",
20 | "express-async-handler": "^1.2.0",
21 | "express-rate-limit": "^7.4.1",
22 | "fs": "^0.0.1-security",
23 | "google-auth-library": "^9.11.0",
24 | "jsonwebtoken": "^9.0.2",
25 | "moment": "^2.30.1",
26 | "mongodb": "^6.9.0",
27 | "mongoose": "^8.4.4",
28 | "multer": "^1.4.5-lts.1",
29 | "nodemailer": "^6.9.15",
30 | "notes-app-backend": "file:"
31 | },
32 | "devDependencies": {
33 | "nodemon": "^3.1.7"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/backend/uploads/profilePhoto-1721713869259-990478442.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/backend/uploads/profilePhoto-1721713869259-990478442.png
--------------------------------------------------------------------------------
/backend/uploads/profilePhoto-1721713968989-129215121.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/backend/uploads/profilePhoto-1721713968989-129215121.jpeg
--------------------------------------------------------------------------------
/backend/uploads/profilePhoto-1721714084138-572334207.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/backend/uploads/profilePhoto-1721714084138-572334207.jpg
--------------------------------------------------------------------------------
/backend/utilities.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const dotenv = require('dotenv')
3 | dotenv.config()
4 | const { ACCESS_TOKEN_SECRET } = process.env;
5 |
6 |
7 | const authenticationToken = (req, res, next) => {
8 | const token = req.headers["authorization"];
9 | if (!token) return res.sendStatus(403);
10 |
11 | jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
12 | if (err) return res.sendStatus(403);
13 | req.user = user;
14 | next();
15 | });
16 | };
17 |
18 |
19 | module.exports = authenticationToken ;
20 |
--------------------------------------------------------------------------------
/backend/utils/const.js:
--------------------------------------------------------------------------------
1 | // HTTP Status Codes
2 | const HTTP_STATUS = {
3 | OK: 200,
4 | CREATED: 201,
5 | BAD_REQUEST: 400,
6 | UNAUTHORIZED: 401,
7 | NOT_FOUND: 404,
8 | INTERNAL_SERVER_ERROR: 500,
9 | };
10 |
11 | // Success Messages
12 | const MESSAGES = {
13 | USER_REGISTERED_SUCCESSFULLY: "Registration Successful",
14 | LOGIN_SUCCESSFUL: "Login Successful",
15 | NOTE_ADDED_SUCCESSFULLY: "Note added successfully",
16 | NOTE_UPDATED_SUCCESSFULLY: "Note updated successfully",
17 | NOTES_FETCHED_SUCCESSFULLY: "Notes fetched successfully",
18 | NOTE_DELETED_SUCCESSFULLY: "Note deleted successfully",
19 | EMAIL_UPDATED_SUCCESSFULLY: "Email updated successfully",
20 | FULLNAME_UPDATED_SUCCESSFULLY: "Name updated successfully",
21 | PHONE_UPDATED_SUCCESSFULLY: "Phone number updated successfully",
22 | PROFILE_PHOTO_UPDATED_SUCCESSFULLY: "Profile photo updated successfully",
23 | GOOGLE_AUTH_SUCCESSFUL: "Google authentication successful",
24 | FEEDBACK_SUBMITTED_SUCCESSFULLY: "Feedback submitted successfully",
25 | };
26 |
27 | // Error Messages
28 | const ERROR_MESSAGES = {
29 | // User Registration Errors
30 | NAME_REQUIRED: "Name is required",
31 | INVALID_NAME_FORMAT: "Invalid Name format",
32 | EMAIL_REQUIRED: "Email is required",
33 | INVALID_EMAIL_FORMAT: "Invalid Email format",
34 | PASSWORD_REQUIRED: "Password is required",
35 | PASSWORD_UPPERCASE_REQUIRED:
36 | "Password must include at least one Uppercase letter",
37 | PASSWORD_LOWERCASE_REQUIRED:
38 | "Password must include at least one Lowercase letter",
39 | PASSWORD_SPECIAL_CHAR_REQUIRED:
40 | "Password must include at least one special character",
41 | PASSWORD_MIN_LENGTH: "Min password length should be 8",
42 | USER_ALREADY_EXISTS: "User already exists",
43 | EMAIL_NOT_VERIFIED: "Email not verified, Please verify your email.",
44 |
45 | // Authentication Errors
46 | EMAIL_PASSWORD_REQUIRED: "Email and Password are required",
47 | INVALID_CREDENTIALS: "Invalid Credentials",
48 | USER_NOT_AUTHENTICATED: "User not authenticated",
49 |
50 | // Note-related Errors
51 | TITLE_CONTENT_REQUIRED: "Title and Content are required",
52 | PROVIDE_FIELD_TO_UPDATE: "Please provide at least one field to update",
53 | NOTE_NOT_FOUND: "Note not found",
54 | PROVIDE_IS_PINNED_FIELD: "Please provide isPinned field",
55 | PROVIDE_SEARCH_QUERY: "Please provide at least one field to search",
56 |
57 | // User-related Errors
58 | USER_NOT_FOUND: "User not found",
59 |
60 | // Google OAuth Errors
61 | INVALID_GOOGLE_TOKEN: "Invalid Google token",
62 |
63 | // Feedback Errors
64 | FAILED_TO_SUBMIT_FEEDBACK: "Failed to submit feedback",
65 |
66 | // General Errors
67 | INTERNAL_SERVER_ERROR: "Internal server error",
68 | };
69 |
70 | export { HTTP_STATUS, MESSAGES, ERROR_MESSAGES };
71 |
--------------------------------------------------------------------------------
/backend/utils/multer.js:
--------------------------------------------------------------------------------
1 | //util
2 | import fs from "fs";
3 | import multer from "multer";
4 | import path from "path";
5 |
6 | export const storage = multer.diskStorage({
7 | destination: (req, file, cb) => {
8 | const uploadPath = path.join(__dirname, "uploads");
9 | if (!fs.existsSync(uploadPath)) {
10 | fs.mkdirSync(uploadPath);
11 | }
12 | cb(null, uploadPath);
13 | },
14 | filename: (req, file, cb) => {
15 | const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
16 | cb(
17 | null,
18 | file.fieldname + "-" + uniqueSuffix + path.extname(file.originalname)
19 | );
20 | },
21 | });
--------------------------------------------------------------------------------
/backend/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "builds": [
4 | {
5 | "src": "*.js",
6 | "use": "@vercel/node"
7 | }
8 | ],
9 | "routes": [
10 | {
11 | "src": "/",
12 | "dest": "/"
13 | },
14 | {
15 | "src": "/landing",
16 | "dest": "/"
17 | },
18 | {
19 | "src": "/dashboard",
20 | "dest": "/"
21 | },
22 | {
23 | "src": "/login",
24 | "dest": "/"
25 | },
26 | {
27 | "src": "/signup",
28 | "dest": "/"
29 | },
30 | {
31 | "src": "/about",
32 | "dest": "/"
33 | },
34 | {
35 | "src": "/my-profile",
36 | "dest": "/"
37 | }
38 | ]
39 | }
--------------------------------------------------------------------------------
/frontend/.env.development:
--------------------------------------------------------------------------------
1 | # Frontend Environment Variables
2 |
3 | # Base URL of the backend server
4 | VITE_BACKEND_URL=http://localhost:5000
5 |
6 | # API key for Google services (if used in frontend)
7 | VITE_REACT_APP_GOOGLE_API_TOKEN=114458585610-q8v3k7sdfbu55j876cj11km7h12tpj02.apps.googleusercontent.com
--------------------------------------------------------------------------------
/frontend/.env.example:
--------------------------------------------------------------------------------
1 | # Frontend Environment Variables
2 |
3 | # Base URL of the backend server
4 | VITE_BACKEND_URL=http://localhost:5000
5 |
6 | # API key for Google services (if used in frontend)
7 | VITE_GOOGLE_CLIENT=your_client_id
8 | VITE_GOOGLE_API_TOKEN=your_api_token
9 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 | Scribbie
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notes-app",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --host 0.0.0.0",
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 | "@emotion/react": "^11.13.3",
14 | "@emotion/styled": "^11.13.0",
15 | "@mui/material": "^6.1.6",
16 | "@react-oauth/google": "^0.12.1",
17 | "axios": "^1.7.2",
18 | "cookies-js": "^1.2.3",
19 | "cors": "^2.8.5",
20 | "dotenv": "^16.4.5",
21 | "framer-motion": "^11.11.6",
22 | "gapi-script": "^1.2.0",
23 | "gsap": "^3.12.5",
24 | "http-proxy-middleware": "^3.0.0",
25 | "js-cookie": "^3.0.5",
26 | "moment": "^2.30.1",
27 | "notes-app": "file:",
28 | "react": "^18.2.0",
29 | "react-dom": "^18.2.0",
30 | "react-hot-toast": "^2.4.1",
31 | "react-icons": "^5.3.0",
32 | "react-modal": "^3.16.1",
33 | "react-quill": "^2.0.0",
34 | "react-router-dom": "^6.26.2",
35 | "react-scroll": "^1.9.0",
36 | "react-simple-scroll-up": "^0.2.3"
37 | },
38 | "devDependencies": {
39 | "@types/react": "^18.2.66",
40 | "@types/react-dom": "^18.2.22",
41 | "@vitejs/plugin-react": "^4.2.1",
42 | "autoprefixer": "^10.4.19",
43 | "eslint": "^8.57.0",
44 | "eslint-plugin-react": "^7.34.1",
45 | "eslint-plugin-react-hooks": "^4.6.0",
46 | "eslint-plugin-react-refresh": "^0.4.6",
47 | "postcss": "^8.4.38",
48 | "tailwindcss": "^3.4.3",
49 | "vite": "^5.2.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/public/logo.png
--------------------------------------------------------------------------------
/frontend/public/testimonial-section.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/public/testimonial-section.png
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Poppins', sans-serif;
3 | }
4 |
5 | .note-content .ql-editor {
6 | font-family: 'Poppins', sans-serif;
7 | }
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import {
3 | BrowserRouter as Router,
4 | Routes,
5 | Route,
6 | Navigate,
7 | useLocation,
8 | } from "react-router-dom";
9 | import Home from "./pages/Home/Home";
10 | import Signup from "./pages/Signup/Signup";
11 | import Login from "./pages/Login/Login";
12 | import Hero from "./components/Hero/Hero";
13 | import About from "./components/About/About";
14 | import Loading from "./components/Loading";
15 | import ProfilePage from "./pages/ProfilePage/ProfilePage";
16 | import Testimonial from "./components/Testimonial";
17 | import Pricing from "./components/Pricing";
18 | import Footer from "./components/sticky_footer/Footer";
19 | import Contact from "./components/Contact/Contact";
20 | import Contributors from "./components/Contributors/Contributors";
21 | import ArchivedNotes from "./components/ArchivedNotes/ArchivedNotes";
22 | import Preloader from "./components/Preloader";
23 | import VerifyEmail from "./pages/ForgotPassword/VerifyEmail";
24 | import VerifyOtp from "./pages/ForgotPassword/VerifyOtp";
25 | import NewPassword from "./pages/ForgotPassword/NewPassword";
26 | import Calendar from "./components/Calendar/Calendar";
27 | import { ScrollToTop } from "react-simple-scroll-up";
28 |
29 | // currently this component is hide
30 | // import Navbar from './components/Navbar';
31 | // import ProtectedRoute from './utils/ProtectedRoute';
32 | // import ErrorPage from './components/ErrorPage';
33 | // import Footer from './components/Footer';
34 |
35 | const App = () => {
36 | const [loading, setLoading] = useState(false);
37 | const [user, setUser] = useState(null);
38 | const location = useLocation();
39 |
40 | useEffect(() => {
41 | const handleRouteChange = () => {
42 | setLoading(true);
43 | setTimeout(() => {
44 | setLoading(false);
45 | }, 500);
46 | };
47 | handleRouteChange();
48 | }, [location]);
49 |
50 | useEffect(() => {
51 | const storedUser = localStorage.getItem("user");
52 | if (storedUser) {
53 | try {
54 | setUser(JSON.parse(storedUser));
55 | } catch (e) {
56 | console.error("Error parsing stored user", e);
57 | }
58 | }
59 | }, []);
60 |
61 | return (
62 |
63 | {/*
*/}
64 | {loading &&
}
65 | {user && location.pathname === "/" ? (
66 |
67 | ) : (
68 | <>
69 |
70 | } />
71 | } />
72 | } />
73 | } />
74 | } />
75 | } />
76 | } />
77 | } />
78 | } />
79 | } />
80 | } />
81 |
82 | } />
83 | } />
84 | } />
85 | } />
86 | } />
87 | {/* } /> */}
88 |
89 | {location.pathname !== "/dashboard" && (
90 |
94 |
95 |
96 | }
97 | size={40}
98 | bgColor="#111827"
99 | strokeWidth={3}
100 | strokeFillColor="#6B7280"
101 | strokeEmptyColor="#CBCBCB"
102 | symbolColor="#fff"
103 | />
104 | )}
105 | >
106 | )}
107 |
108 | );
109 | };
110 |
111 | const AppWithRouter = () => (
112 |
113 |
114 |
115 | );
116 |
117 | export default AppWithRouter;
--------------------------------------------------------------------------------
/frontend/src/assets/images/add-notes.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/addPost.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/cross.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/hero1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/src/assets/images/hero1.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/images/hero2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/src/assets/images/hero2.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/hero3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/src/assets/images/hero3.png
--------------------------------------------------------------------------------
/frontend/src/assets/images/logo/dark-logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/src/assets/images/logo/dark-logo.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/images/logo/whiteLogo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/src/assets/images/logo/whiteLogo.jpg
--------------------------------------------------------------------------------
/frontend/src/assets/images/no-data.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/images/noFound.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/components/Cards/NoteCard.jsx:
--------------------------------------------------------------------------------
1 | import ReactQuill from "react-quill";
2 | import "react-quill/dist/quill.snow.css";
3 | import PropTypes from "prop-types";
4 | import moment from "moment";
5 | import {
6 | MdCreate,
7 | MdDelete,
8 | MdOutlinePushPin,
9 | MdCheckBox,
10 | MdCheckBoxOutlineBlank,
11 | MdDownload,
12 | MdShare, // Import Share Icon
13 | } from "react-icons/md";
14 |
15 | const getContrastColor = (background) => {
16 | const hex = background.replace("#", "");
17 | const r = parseInt(hex.substr(0, 2), 16);
18 | const g = parseInt(hex.substr(2, 2), 16);
19 | const b = parseInt(hex.substr(4, 2), 16);
20 | const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
21 |
22 | return luminance > 0.5 ? "black" : "white"; // Return black for light backgrounds, white for dark
23 | };
24 |
25 | const truncateContent = (content) => {
26 | const words = content.split(" ");
27 | return words.length > 25 ? words.slice(0, 25).join(" ") + "..." : content;
28 | };
29 |
30 | const NoteCard = ({
31 | id,
32 | title,
33 | date,
34 | content,
35 | tags,
36 | isPinned,
37 | background,
38 | onEdit,
39 | onDelete,
40 | onPinNote,
41 | onClick,
42 | isSelected,
43 | onSelect,
44 | }) => {
45 | const textColor = getContrastColor(background);
46 |
47 | // Function to download the note
48 | const downloadNote = (content, title, date) => {
49 | const strippedContent = content.replace(/<[^>]+>/g, '').trim();
50 | const formattedDate = moment(date).format("Do MMM YYYY");
51 | const textToDownload = `Title: ${title}\nDate of Creation: ${formattedDate}\nContent: ${strippedContent}`;
52 | const blob = new Blob([textToDownload], { typze: "text/plain;charset=utf-8" });
53 | const link = document.createElement("a");
54 | link.href = URL.createObjectURL(blob);
55 | link.download = `${title}.txt`;
56 | link.click();
57 | };
58 |
59 | const apiBaseUrl = import.meta.env.VITE_BACKEND_URL;
60 |
61 |
62 | // Share the note by copying the link to the clipboard
63 | const shareNote = (id, title) => {
64 | const noteLink = `${apiBaseUrl}/view-note/${id}`; // Modify based on your app's route
65 | navigator.clipboard.writeText(noteLink).then(() => {
66 | alert(`Note link copied: ${noteLink}`);
67 | });
68 | };
69 |
70 | return (
71 |
75 |
76 |
77 |
{title}
78 |
79 | {moment(date).format("Do MMM YYYY")}
80 |
81 |
82 |
83 |
84 |
{
87 | e.stopPropagation();
88 | onPinNote();
89 | }}
90 | />
91 |
92 | {isPinned ? "Unpin Note" : "Pin Note"}
93 |
94 |
95 |
96 |
97 |
110 |
111 |
112 |
113 | {tags.length > 0 &&
114 | tags.map((tag, index) => (
115 |
119 | {tag !== "" ? `#${tag}` : ""}
120 |
121 | ))}
122 |
123 |
124 |
125 |
126 |
{
129 | e.stopPropagation();
130 | onEdit();
131 | }}
132 | />
133 |
134 | {"Edit Note"}
135 |
136 |
137 |
138 |
139 |
{
142 | e.stopPropagation();
143 | onDelete();
144 | }}
145 | />
146 |
147 | {"Delete Note"}
148 |
149 |
150 |
151 | {/* Export Icon (Download Button) */}
152 |
153 |
{
156 | e.stopPropagation();
157 | downloadNote(content, title, date);
158 | }}
159 | />
160 |
161 | {"Export Note"}
162 |
163 |
164 |
165 | {/* Share Icon */}
166 |
167 |
{
170 | e.stopPropagation();
171 | shareNote(id, title);
172 | }}
173 | />
174 |
175 | {"Share Note"}
176 |
177 |
178 |
179 |
{
181 | e.stopPropagation();
182 | onSelect(id);
183 | }}
184 | >
185 |
186 | {isSelected ? (
187 |
188 | ) : (
189 |
190 | )}
191 |
192 |
193 |
194 |
195 |
196 | );
197 | };
198 |
199 | NoteCard.propTypes = {
200 | title: PropTypes.string.isRequired,
201 | date: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired,
202 | content: PropTypes.string.isRequired,
203 | tags: PropTypes.arrayOf(PropTypes.string).isRequired,
204 | isPinned: PropTypes.bool,
205 | background: PropTypes.string,
206 | onEdit: PropTypes.func,
207 | onDelete: PropTypes.func,
208 | onPinNote: PropTypes.func,
209 | onClick: PropTypes.func,
210 | isSelected: PropTypes.bool,
211 | onSelect: PropTypes.func,
212 | };
213 |
214 | NoteCard.defaultProps = {
215 | isPinned: false,
216 | background: "#ffffff",
217 | onEdit: () => { },
218 | onDelete: () => { },
219 | onPinNote: () => { },
220 | onClick: () => { },
221 | isSelected: false,
222 | onSelect: () => { },
223 | };
224 |
225 | export default NoteCard;
226 |
--------------------------------------------------------------------------------
/frontend/src/components/Cards/ProfileInfo.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { getInitials } from "../../utils/helper";
3 | import { useNavigate } from "react-router-dom";
4 | import { CiUser, CiCircleInfo, CiLogout } from "react-icons/ci";
5 | import { MdOutlineArchive } from "react-icons/md";
6 | import { FaRegTrashAlt } from "react-icons/fa";
7 |
8 | const ProfileInfo = ({ userInfo, onLogout }) => {
9 | const [isDropdownOpen, setIsDropdownOpen] = useState(false);
10 | const [isModalOpen, setIsModalOpen] = useState(false);
11 | const navigate = useNavigate();
12 | const dropdownRef = useRef(null);
13 |
14 | const toggleDropdown = () => {
15 | setIsDropdownOpen(!isDropdownOpen);
16 | };
17 |
18 | // const handleMyProfile = () => {
19 | // navigate("/my-profile");
20 | // setIsDropdownOpen(false);
21 | // };
22 |
23 | const handleArchivedNotes = () => {
24 | navigate("/archived-notes");
25 | setIsDropdownOpen(false)
26 | }
27 |
28 | const handleAbout = () => {
29 | navigate("/about");
30 | setIsDropdownOpen(false);
31 | };
32 |
33 | const handleLogout = () => {
34 | setIsDropdownOpen(false);
35 | setIsModalOpen(true); // Open the confirmation modal
36 | };
37 |
38 | const confirmLogout = () => {
39 | onLogout();
40 | setIsModalOpen(false);
41 | setIsDropdownOpen(false);
42 | };
43 |
44 | const closeModal = () => {
45 | setIsModalOpen(false);
46 | };
47 |
48 | const handleClickOutside = (event) => {
49 | if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
50 | setIsDropdownOpen(false);
51 | }
52 | };
53 |
54 | useEffect(() => {
55 | if (isDropdownOpen) {
56 | document.addEventListener("click", handleClickOutside);
57 | } else {
58 | document.removeEventListener("click", handleClickOutside);
59 | }
60 |
61 | return () => {
62 | document.removeEventListener("click", handleClickOutside);
63 | };
64 | }, [isDropdownOpen]);
65 |
66 | return (
67 |
68 |
72 | {getInitials(userInfo?.fullName)}
73 |
74 |
75 | {isDropdownOpen && (
76 |
77 | {/*
81 |
82 |
83 |
84 | Profile Settings
85 | */}
86 | {/*
90 |
91 |
92 |
93 | Archived Notes
94 | */}
95 |
99 |
100 |
101 |
102 | About
103 |
104 |
108 |
109 |
110 |
111 | Logout
112 |
113 |
114 | )}
115 |
116 | {/* Modal for logout confirmation */}
117 | {isModalOpen && (
118 |
119 |
120 |
121 |
Confirm Logout
122 |
Are you sure you want to log out?
123 |
124 |
128 | Cancel
129 |
130 |
134 |
135 | Logout
136 |
137 |
138 |
139 |
140 | )}
141 |
142 | );
143 | };
144 |
145 | export default ProfileInfo;
146 |
--------------------------------------------------------------------------------
/frontend/src/components/CircularLoader.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const CircularLoader = () => {
4 | return (
5 |
8 | );
9 | };
10 |
11 | export default CircularLoader;
12 |
--------------------------------------------------------------------------------
/frontend/src/components/Contact/Contact.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, useState } from "react";
2 | import Navbar from "../Navbar";
3 | import { MdFacebook } from "react-icons/md";
4 | import { FaLinkedin } from "react-icons/fa";
5 | import { IoLogoInstagram } from "react-icons/io5";
6 | import { FaXTwitter } from "react-icons/fa6";
7 | import axiosInstance from "../../utils/axiosInstance";
8 | import toast from "react-hot-toast";
9 | import Backdrop from "@mui/material/Backdrop";
10 | import CircularProgress from "@mui/material/CircularProgress";
11 |
12 | const Contact = () => {
13 | const [data, setData] = useState({
14 | first_name: "",
15 | last_name: "",
16 | user_email: "",
17 | message: "",
18 | });
19 | const [loading, setLoading] = useState(false); // State for loading indicator
20 |
21 | const changeHandler = (e) => {
22 | setData((prevData) => ({
23 | ...prevData,
24 | [e.target.name]: e.target.value,
25 | }));
26 | };
27 |
28 | const submitHandler = async (e) => {
29 | e.preventDefault();
30 | console.log("FORM DATA----: ", data);
31 |
32 | if (!data.user_email || !data.first_name || !data.message) {
33 | toast.error("All Fields are required :)");
34 | return;
35 | }
36 |
37 | setLoading(true);
38 | try {
39 | const response = await axiosInstance.post("/contact", data);
40 |
41 | if (response.data.error) {
42 | toast.error("Failed submission");
43 | } else {
44 | toast.success("Will connect to you soon");
45 | setData({ first_name: "", last_name: "", user_email: "", message: "" });
46 | }
47 | } catch (error) {
48 | toast.error("Submission failed");
49 | } finally {
50 | setLoading(false); // Hide loading indicator
51 | }
52 | };
53 |
54 | useEffect(() => {
55 | window.scrollTo(0, 0);
56 | }, []);
57 |
58 | const form = useRef();
59 | const user = JSON.parse(localStorage.getItem("user"));
60 |
61 | return (
62 |
63 |
64 |
65 |
66 |
Get in touch
67 |
68 |
Visit us
69 |
Come say hello at our office HQ.
70 |
71 | 67 Wistoria Way Croydon South VIC 3136 AU
72 |
73 |
74 |
75 |
Chat to us
76 |
Our friendly team is here to help.
77 |
@Scribbie.com
78 |
79 |
80 |
Call us
81 |
Mon-Fri from 8am to 5pm
82 |
(+995) 555-55-55-55
83 |
84 |
85 |
Social media
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
174 |
175 |
176 | {/* MUI Backdrop with CircularProgress */}
177 |
theme.zIndex.drawer + 1 }} open={loading}>
178 |
179 |
180 |
181 | );
182 | };
183 |
184 | export default Contact;
185 |
--------------------------------------------------------------------------------
/frontend/src/components/Contributors/Contributors.css:
--------------------------------------------------------------------------------
1 | /* Overall container */
2 | .contributors-container {
3 | width: 100%;
4 | height: 100%;
5 | padding: 4rem 2rem;
6 | overflow: hidden;
7 | background-color: #f4f5f7;
8 | }
9 |
10 | /* Title Styles */
11 | .contributors-title {
12 | text-align: center;
13 | color: #333;
14 | font-size: 2.8rem;
15 | font-weight: 700;
16 | margin-bottom: 3rem;
17 | letter-spacing: 1px;
18 | text-transform: uppercase;
19 | font-family: 'Poppins', sans-serif;
20 | }
21 |
22 | /* Grid for contributors */
23 | .contributors-grid {
24 | display: flex;
25 | flex-wrap: wrap;
26 | justify-content: center;
27 | gap: 3rem;
28 | margin-bottom: 4rem;
29 | }
30 |
31 | /* Contributor Card */
32 | .contributor-card {
33 | position: relative;
34 | width: 100%;
35 | max-width: 250px;
36 | background-color: #ffffff;
37 | border: 2px solid #ddd;
38 | border-radius: 12px;
39 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
40 | padding: 1.5rem;
41 | text-align: center;
42 | transition: all 0.3s ease-in-out;
43 | overflow: hidden;
44 | font-family: 'Roboto', sans-serif;
45 | cursor: pointer; /* Make the card appear clickable */
46 | }
47 |
48 | .contributor-card:hover {
49 | transform: translateY(-5px);
50 | box-shadow: 0 6px 18px rgba(0, 0, 0, 0.2);
51 | }
52 |
53 | /* Avatar */
54 | .contributor-avatar {
55 | width: 6rem;
56 | height: 6rem;
57 | border-radius: 50%;
58 | object-fit: cover;
59 | border: 4px solid #00b4d8;
60 | margin-bottom: 1.5rem;
61 | transition: transform 0.3s ease, border-color 0.3s ease;
62 | }
63 |
64 | .contributor-avatar:hover {
65 | transform: scale(1.1);
66 | border-color: #0299a5;
67 | }
68 |
69 | /* Contributor Name */
70 | .contributor-name {
71 | font-size: 1.2rem;
72 | font-weight: 600;
73 | color: #333;
74 | margin-bottom: 0.5rem;
75 | transition: color 0.3s ease;
76 | }
77 |
78 | /* Contributions Text */
79 | .contributor-contributions {
80 | color: #555;
81 | font-size: 0.9rem;
82 | font-weight: 500;
83 | margin-top: 0.5rem;
84 | transition: color 0.3s ease;
85 | }
86 |
87 | /* Hover Effects for Text */
88 | .contributor-card:hover .contributor-name {
89 | color: #00b4d8;
90 | }
91 |
92 | .contributor-card:hover .contributor-contributions {
93 | color: #444;
94 | font-weight: 600;
95 | }
96 |
97 | /* Media Queries for Responsiveness */
98 | @media (max-width: 1200px) {
99 | .contributor-card {
100 | max-width: 220px;
101 | }
102 | }
103 |
104 | @media (max-width: 992px) {
105 | .contributor-card {
106 | max-width: 45%;
107 | }
108 | }
109 |
110 | @media (max-width: 768px) {
111 | .contributor-card {
112 | max-width: 95%;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/frontend/src/components/Contributors/Contributors.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import axios from "axios";
3 | import "./Contributors.css";
4 |
5 | function Contributors() {
6 | const [contributors, setContributors] = useState([]);
7 | const [loading, setLoading] = useState(true);
8 | const [error, setError] = useState(null);
9 |
10 | useEffect(() => {
11 | async function fetchContributors() {
12 | let allContributors = [];
13 | let page = 1;
14 |
15 | try {
16 | while (true) {
17 | const response = await axios.get(
18 | `https://api.github.com/repos/Scribbie-Notes/notes-app/contributors`,
19 | {
20 | params: {
21 | per_page: 100,
22 | page,
23 | },
24 | }
25 | );
26 | const data = response.data;
27 | if (data.length === 0) {
28 | break;
29 | }
30 | allContributors = [...allContributors, ...data];
31 | page++;
32 | }
33 | setContributors(allContributors);
34 | } catch (error) {
35 | console.error("Error fetching contributors:", error);
36 | setError("Failed to load contributors. Please try again later.");
37 | } finally {
38 | setLoading(false);
39 | }
40 | }
41 |
42 | fetchContributors();
43 | }, []);
44 |
45 | if (loading) {
46 | return Loading contributors...
;
47 | }
48 |
49 | return (
50 |
51 |
Our Contributors
52 | {error &&
{error}
}
53 |
78 |
79 | );
80 | }
81 |
82 | export default Contributors;
83 |
--------------------------------------------------------------------------------
/frontend/src/components/EmptyCard/EmptyCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const EmptyCard = ({ imgSrc, message }) => {
4 | return (
5 |
6 |
7 |
8 |
9 | {message}
10 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default EmptyCard;
17 |
--------------------------------------------------------------------------------
/frontend/src/components/ErrorPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useLocation } from "react-router-dom";
3 |
4 | const ErrorPage = () => {
5 | const location = useLocation();
6 | const errorCode = location.state?.errorCode || 404;
7 | const errorMessage = location.state?.errorMessage || "We can't find that page.";
8 |
9 |
10 | return (
11 |
29 | );
30 | };
31 |
32 | export default ErrorPage;
33 |
--------------------------------------------------------------------------------
/frontend/src/components/Faq.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Cross from "../assets/images/cross.svg";
3 |
4 | const AccordionItem = ({ question, answer, isOpen, onToggle }) => {
5 | return (
6 |
7 |
15 |
{question}
16 |
21 |
22 |
27 |
30 | {answer}
31 |
32 |
33 |
34 | );
35 | };
36 |
37 | const Faq = () => {
38 | const [openIndex, setOpenIndex] = useState(null);
39 |
40 | const faqData = [
41 | {
42 | question: "Q. What is Scribbie?",
43 | answer: "Scribbie is a powerful, intuitive note-taking website designed specifically for working professionals. It helps you manage your notes seamlessly and effectively, making it easy to stay organized and focused.",
44 | },
45 | {
46 | question: "Q. How do I get started with Scribbie?",
47 | answer: "Getting started with Scribbie is easy! Simply sign up for an account on our website, and you can start taking notes right away. Explore the features and customize your experience to fit your needs.",
48 | },
49 | {
50 | question: "Q. How does Scribbie help with note management?",
51 | answer: "Scribbie allows you to categorize and tag your notes for quick retrieval, making it easy to find information when you need it. The intuitive interface ensures you can organize your notes in a way that suits your personal workflow.",
52 | },
53 | {
54 | question: "Q. Can I access Scribbie on multiple devices?",
55 | answer: "Yes! Scribbie is a web-based application, so you can access your notes from any device with an internet connection. Your notes are automatically synced, ensuring you have access wherever you are.",
56 | },
57 | {
58 | question: "Q. Is my data secure on Scribbie?",
59 | answer: "Yes, we take data security seriously. Scribbie uses encryption and other security measures to protect your notes and ensure that your information is safe and private.",
60 | }
61 | ];
62 |
63 | const handleToggle = (index) => {
64 | setOpenIndex(openIndex === index ? null : index);
65 | };
66 |
67 | return (
68 |
69 |
70 | Frequently Asked Questions
71 |
72 |
73 | {faqData.map((item, index) => (
74 |
{item.question}}
77 | answer={{item.answer} }
78 | isOpen={openIndex === index}
79 | onToggle={() => handleToggle(index)}
80 | />
81 | ))}
82 |
83 |
84 | );
85 | };
86 |
87 | export default Faq;
88 |
--------------------------------------------------------------------------------
/frontend/src/components/FilterTags.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const FilterTags = ({ allNotes, filterSetNotes }) => {
4 | const [allTags, setAllTags] = useState([]);
5 | const [filteredTags, setFilteredTags] = useState([]);
6 | const [selectedTags, setSelectedTags] = useState([]);
7 | const [searchTerm, setSearchTerm] = useState("");
8 |
9 | // Extract unique tags from notes
10 | useEffect(() => {
11 | const tagsSet = new Set();
12 | allNotes.forEach((note) => {
13 | note.tags.forEach((tag) => {
14 | if (tag !== "") {
15 | tagsSet.add(tag);
16 | }
17 | });
18 | });
19 |
20 | const uniqueTags = Array.from(tagsSet);
21 | setAllTags(uniqueTags);
22 | setFilteredTags(uniqueTags); // Initialize filtered tags with all unique tags
23 | }, [allNotes]);
24 |
25 | // Handle search input change
26 | const handleSearchChange = (event) => {
27 | const value = event.target.value;
28 | setSearchTerm(value);
29 |
30 | // Filter tags based on search term
31 | const filtered = allTags.filter((tag) =>
32 | tag.toLowerCase().includes(value.toLowerCase())
33 | );
34 | setFilteredTags(filtered);
35 | };
36 |
37 | // Handle tag selection/deselection
38 | const handleTagClick = (tag) => {
39 | let updatedSelectedTags;
40 |
41 | if (selectedTags.includes(tag)) {
42 | // If tag is already selected, remove it
43 | updatedSelectedTags = selectedTags.filter((t) => t !== tag);
44 | } else {
45 | // Add new tag to the selected tags
46 | updatedSelectedTags = [...selectedTags, tag];
47 | }
48 |
49 | setSelectedTags(updatedSelectedTags);
50 |
51 | // Filter notes based on selected tags
52 | if (updatedSelectedTags.length > 0) {
53 | const filteredNotes = allNotes.filter((note) =>
54 | updatedSelectedTags.every((selectedTag) =>
55 | note.tags.includes(selectedTag)
56 | )
57 | );
58 | filterSetNotes(filteredNotes);
59 | } else {
60 | // If no tags are selected, reset to show all notes
61 | filterSetNotes(allNotes);
62 | }
63 | };
64 |
65 | return (
66 |
68 | {/* Search and Filter Tags Section (Left) */}
69 |
70 | {/* Search Input */}
71 |
78 |
79 | {/* Filtered Tags List */}
80 |
81 | {filteredTags.slice(0, 5).map((tag, index) => (
82 | handleTagClick(tag)}
85 | className={`tag px-2 py-1 rounded-md cursor-pointer ${
86 | selectedTags.includes(tag)
87 | ? "bg-blue-500 text-white"
88 | : "bg-gray-200 text-gray-700 hover:bg-gray-300"
89 | } transition`}
90 | >
91 | #{tag}
92 |
93 | ))}
94 |
95 |
96 |
97 | {/* Selected Tags Display (Right) */}
98 |
99 |
Selected Tags:
100 |
101 | {selectedTags.length > 0 ? (
102 | selectedTags.map((tag, index) => (
103 | handleTagClick(tag)}
106 | className="tag bg-blue-500 text-white px-2 py-1 rounded-md cursor-pointer hover:bg-blue-600 transition"
107 | >
108 | #{tag}
109 |
110 | ))
111 | ) : (
112 | No tags selected
113 | )}
114 |
115 |
116 |
117 |
118 |
119 | );
120 | };
121 |
122 | export default FilterTags;
123 |
--------------------------------------------------------------------------------
/frontend/src/components/Footer/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Scribbie-Notes/notes-app/a5a6d6d0fb634a3b6605a00f37fad8895838df55/frontend/src/components/Footer/logo.png
--------------------------------------------------------------------------------
/frontend/src/components/GoogleTranslate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 |
3 | const GoogleTranslate = () => {
4 | useEffect(() => {
5 | window.googleTranslateInit = () => {
6 | if (!window.google?.translate?.TranslateElement) {
7 | setTimeout(window.googleTranslateInit, 100);
8 | } else {
9 | new window.google.translate.TranslateElement(
10 | {
11 | pageLanguage: "en",
12 | includedLanguages:
13 | "en,hi,pa,sa,mr,ur,bn,es,ja,ko,zh-CN,es,nl,fr,de,it,ta,te",
14 | layout:
15 | window.google.translate.TranslateElement.InlineLayout.HORIZONTAL,
16 | defaultLanguage: "en",
17 | autoDisplay: false,
18 | },
19 | "google_element"
20 | );
21 | }
22 | cleanUpGadgetText();
23 | };
24 |
25 | const loadGoogleTranslateScript = () => {
26 | if (!document.getElementById("google_translate_script")) {
27 | const script = document.createElement("script");
28 | script.type = "text/javascript";
29 | script.src =
30 | "https://translate.google.com/translate_a/element.js?cb=googleTranslateInit";
31 | script.id = "google_translate_script";
32 | script.onerror = () =>
33 | console.error("Error loading Google Translate script");
34 | document.body.appendChild(script);
35 | }
36 | };
37 |
38 | const cleanUpGadgetText = () => {
39 | const gadgetElement = document.querySelector(".goog-te-gadget");
40 | if (gadgetElement) {
41 | const textNodes = gadgetElement.childNodes;
42 | textNodes.forEach((node) => {
43 | if (node.nodeType === Node.TEXT_NODE) {
44 | node.textContent = ""; // Clear text content
45 | }
46 | });
47 | }
48 | };
49 | loadGoogleTranslateScript();
50 |
51 | if (window.google && window.google.translate) {
52 | window.googleTranslateInit();
53 | }
54 |
55 | return () => {
56 | // Cleanup logic if necessary
57 | };
58 | }, []);
59 |
60 | return (
61 |
65 |
146 |
147 | );
148 | };
149 |
150 | export default GoogleTranslate;
151 |
--------------------------------------------------------------------------------
/frontend/src/components/Hero/Hero.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react';
2 | import Navbar2 from '../Navbar-2';
3 | import { Link } from 'react-router-dom';
4 | // import gsap from 'gsap';
5 | // import { ScrollTrigger } from 'gsap/ScrollTrigger';
6 | import hero2 from "../../assets/images/hero2.png";
7 | import { SiTicktick } from "react-icons/si";
8 | import { TbEyeSearch } from "react-icons/tb";
9 | import { FaSync } from "react-icons/fa";
10 | import Testimonial from '../Testimonial';
11 | import Footer from '../Footer';
12 | import Pricing from '../Pricing';
13 | import Faq from '../Faq';
14 | // import ScrollOnTop from '../Scroll-on-top/ScrollOnTop';
15 | import './styles.css';
16 |
17 | // Register ScrollTrigger plugin
18 | // gsap.registerPlugin(ScrollTrigger);
19 |
20 | const Hero = () => {
21 | const user = JSON.parse(localStorage.getItem("user"));
22 | const heroText = useRef();
23 | const heroParagraph = useRef();
24 | const getStartedButton = useRef(); // Ref for the button
25 | const featureCards = useRef([]);
26 | const sectionRef = useRef();
27 | const whiteSectionRef = useRef(); // Ref for the bg-white section
28 |
29 | return (
30 |
31 |
32 |
33 |
34 |
35 |
36 |
40 | Capture Ideas,
41 |
42 |
43 | Unleash Productivity.
44 |
45 |
49 | Capture every thought and organize with ease. Unlock your full potential with Scribbie
50 |
51 |
52 |
57 | Get Started
58 |
64 |
69 |
70 |
71 |
72 |
73 |
74 | {/* Hero Image on Desktop, Hidden on Mobile */}
75 |
76 |
77 |
78 |
79 | {/* Hero Image on Mobile */}
80 | {/*
81 |
82 |
*/}
83 |
84 |
85 |
86 |
87 |
88 | Unlock Powerful Features to
89 |
90 |
91 | Enhance Your Note-Taking Experience
92 |
93 |
94 |
95 |
(featureCards.current[0] = el)}
97 | className="bg-white p-6 rounded-lg shadow-md hover:shadow-xl transition duration-300 ease-in-out border border-gray-200"
98 | >
99 |
100 |
101 |
Organize Effortlessly
102 |
103 |
104 | Keep your notes neatly categorized with customizable folders and tags.
105 |
106 |
107 |
108 |
(featureCards.current[1] = el)}
110 | className="bg-white p-6 rounded-lg shadow-md hover:shadow-xl transition duration-300 ease-in-out border border-gray-200"
111 | >
112 |
113 |
114 |
Search with Ease
115 |
116 |
117 | Quickly find any note with a powerful, real-time search engine.
118 |
119 |
120 |
121 |
(featureCards.current[2] = el)}
123 | className="bg-white p-6 rounded-lg shadow-md hover:shadow-xl transition duration-300 ease-in-out border border-gray-200"
124 | >
125 |
126 |
127 |
Sync Across Devices
128 |
129 |
130 | Seamlessly sync your notes between all your devices.
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | {/*
*/}
142 |
143 | );
144 | }
145 |
146 | export default Hero;
--------------------------------------------------------------------------------
/frontend/src/components/Hero/styles.css:
--------------------------------------------------------------------------------
1 | html {
2 | scroll-behavior: smooth;
3 | }
4 |
5 | body {
6 | font-family: 'Poppins', sans-serif;
7 | }
8 |
9 | .note-content .ql-editor {
10 | font-family: 'Poppins', sans-serif;
11 | }
--------------------------------------------------------------------------------
/frontend/src/components/Input/AddAttachmentInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import { MdAdd } from 'react-icons/md';
3 |
4 | const AddAttachmentsInput = ({ onFileUpload }) => {
5 | const [files, setFiles] = useState([]); // Store multiple files
6 | const fileInputRef = useRef(null); // Create a ref for the file input
7 |
8 | const handleFileChange = (event) => {
9 | const selectedFiles = Array.from(event.target.files); // Get the selected files
10 | setFiles(selectedFiles); // Update the state with the selected files
11 | };
12 |
13 | const handleButtonClick = () => {
14 | if (files.length > 0) {
15 | console.log('Files selected:', files);
16 | onFileUpload(files); // Call the function passed from the parent
17 | setFiles([]); // Clear the input and state
18 | fileInputRef.current.value = ''; // Clear the file input field
19 | } else {
20 | console.log('No files selected');
21 | }
22 | };
23 |
24 | return (
25 |
26 |
33 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default AddAttachmentsInput;
44 |
--------------------------------------------------------------------------------
/frontend/src/components/Input/PasswordInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import { FaRegEye, FaRegEyeSlash } from 'react-icons/fa';
3 |
4 | const PasswordInput = ({ value, onChange, placeholder }) => {
5 |
6 | const [isShowPassword, setIsShowPassword] = useState(false);
7 |
8 | const toggleShowPassword = () => {
9 | setIsShowPassword(!isShowPassword);
10 | }
11 |
12 | return (
13 |
14 |
21 |
22 | {isShowPassword ? toggleShowPassword()}
26 | /> : toggleShowPassword()}
30 | />
31 |
32 | }
33 |
34 |
35 | )
36 | }
37 |
38 | export default PasswordInput
--------------------------------------------------------------------------------
/frontend/src/components/Input/TagInput.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { MdAdd, MdClose } from "react-icons/md";
3 |
4 | const TagInput = ({ tags, setTags }) => {
5 | const [inputValue, setInputValue] = useState("");
6 |
7 | const handleInputChange = (e) => {
8 | setInputValue(e.target.value);
9 | };
10 |
11 | const addNewTag = () => {
12 | if (inputValue.trim() !== "") {
13 | setTags([...tags, inputValue.trim()]);
14 |
15 | setInputValue("");
16 | }
17 | };
18 |
19 | const handleKeyDown = (e) => {
20 | if (e.key === "Enter") {
21 | addNewTag();
22 | }
23 | };
24 |
25 | const handleRemoveTag = (tagToRemove) => {
26 | setTags(tags.filter((tag) => tag !== tagToRemove));
27 | };
28 |
29 | return (
30 |
31 | {tags?.length > 0 && (
32 |
33 | {tags.map((tag, index) => (
34 |
38 | #{tag}
39 | {
41 | handleRemoveTag(tag);
42 | }}
43 | >
44 |
45 |
46 |
47 | ))}
48 |
49 |
50 | )}
51 |
52 |
60 |
61 | {
64 | addNewTag();
65 | }}
66 | >
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default TagInput;
75 |
--------------------------------------------------------------------------------
/frontend/src/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 |
{/* Semi-transparent background */}
7 |
8 |
9 | );
10 | };
11 |
12 | export default Loading;
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | // components/Modal.js
2 | import React, { useEffect } from "react";
3 |
4 | const Modal = ({ isOpen, onClose, event, onDelete }) => {
5 | if (!isOpen || !event) return null;
6 |
7 | // Close modal on Esc key press
8 | useEffect(() => {
9 | const handleKeyDown = (e) => {
10 | if (e.key === "Escape") {
11 | onClose();
12 | }
13 | };
14 | document.addEventListener("keydown", handleKeyDown);
15 | return () => document.removeEventListener("keydown", handleKeyDown);
16 | }, [onClose]);
17 |
18 | return (
19 |
25 |
26 | {/* Date in top-right corner */}
27 |
28 | {event.date}
29 |
30 |
31 |
36 | {event.title}
37 |
38 |
42 | {event.description || "No description provided."}
43 |
44 |
45 |
46 |
50 | Close
51 |
52 | onDelete(event._id)}
54 | className="bg-red-500 hover:bg-red-600 text-white p-2 rounded-lg transition-all duration-300"
55 | >
56 | Delete
57 |
58 |
59 |
60 |
61 | );
62 | };
63 |
64 | export default Modal;
65 |
--------------------------------------------------------------------------------
/frontend/src/components/Navbar-2.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from "react";
2 | import { useNavigate, useLocation, Link } from "react-router-dom";
3 | import ProfileInfo from "./Cards/ProfileInfo";
4 | import SearchBar from "./SearchBar/SearchBar";
5 | import { toast } from "react-hot-toast";
6 | import gsap from "gsap/all";
7 | import { FiMenu } from "react-icons/fi";
8 | import { SlideTabsExample } from "./Tabs";
9 |
10 | const Navbar2 = ({ userInfo, onSearchNote, handleClearSearch }) => {
11 | const [theme, setTheme] = useState("light");
12 | const [searchQuery, setSearchQuery] = useState("");
13 | const [tagQuery, setTagQuery] = useState("");
14 | const [searchType, setSearchType] = useState("text");
15 | const [isMenuOpen, setIsMenuOpen] = useState(false);
16 | const navigate = useNavigate();
17 | const location = useLocation();
18 | const logoRef = useRef(null);
19 | const searchBarRef = useRef(null);
20 | const profileRef = useRef(null);
21 | const loginButtonRef = useRef(null);
22 | const signupButtonRef = useRef(null);
23 |
24 | useEffect(() => {
25 | const savedTheme = localStorage.getItem("theme");
26 | if (savedTheme) {
27 | setTheme(savedTheme);
28 | }
29 | }, []);
30 |
31 | const toggleTheme = () => {
32 | setTheme((prevTheme) => {
33 | const newTheme = prevTheme === "light" ? "dark" : "light";
34 | localStorage.setItem("theme", newTheme);
35 | return newTheme;
36 | });
37 | };
38 |
39 | const toggleMenu = () => {
40 | setIsMenuOpen(!isMenuOpen);
41 | };
42 |
43 | // useEffect(() => {
44 | // gsap.fromTo(logoRef.current, { y: -20, opacity: 0, scale: 0.8 }, { duration: 1, y: 0, opacity: 1, scale: 1, ease: "power3.out" });
45 | // gsap.fromTo(searchBarRef.current, { x: 50, opacity: 0 }, { duration: 1, x: 0, opacity: 1, ease: "power3.out", delay: 0.5 });
46 |
47 | // if (loginButtonRef.current) {
48 | // gsap.fromTo(loginButtonRef.current, { opacity: 0, y: 20 }, { duration: 1, opacity: 1, y: 0, ease: "bounce.out", delay: 1.7 });
49 | // }
50 | // if (signupButtonRef.current) {
51 | // gsap.fromTo(signupButtonRef.current, { opacity: 0, y: 20 }, { duration: 1, opacity: 1, y: 0, ease: "bounce.out", delay: 1.5 });
52 | // }
53 | // }, []);
54 |
55 | const onLogout = () => {
56 | localStorage.clear();
57 | navigate("/login");
58 | toast.success("Logged out successfully");
59 | };
60 |
61 | const onClearSearch = () => {
62 | setSearchQuery("");
63 | setTagQuery("");
64 | handleClearSearch();
65 | };
66 |
67 | const handleSearch = () => {
68 | if (searchType === "text" && !searchQuery.trim()) {
69 | toast.error("Please enter a search term.");
70 | return;
71 | }
72 |
73 | if (searchType === "tag" && !tagQuery.trim()) {
74 | toast.error("Please enter a tag to search.");
75 | return;
76 | }
77 |
78 | onSearchNote(searchType === "text" ? searchQuery : tagQuery, searchType);
79 | };
80 |
81 | return (
82 |
83 |
84 |
85 |
86 |
cribbie
87 |
88 |
89 |
90 |
91 |
92 |
93 | {/*
94 | setSearchQuery(target.value)}
99 | onTagChange={({ target }) => setTagQuery(target.value)}
100 | onSearchTypeChange={({ target }) => setSearchType(target.value)}
101 | handleSearch={handleSearch}
102 | onClearSearch={onClearSearch}
103 | />
104 |
*/}
105 |
106 |
107 | {userInfo ? (
108 |
109 | ) : (
110 |
111 | {location.pathname !== "/login" && navigate("/login")} className=" text-white bg-gray-800 hover:bg-gray-700 transition duration-300 ease-in-out font-medium rounded-lg text-md px-4 py-1.5">Login }
112 | {/* {location.pathname !== "/signup" && navigate("/signup")} className="text-zinc-200 bg-black rounded-md py-2 px-3">Signup } */}
113 |
114 | )}
115 |
116 |
117 | );
118 | };
119 |
120 | export default Navbar2;
--------------------------------------------------------------------------------
/frontend/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useRef } from "react";
2 | import { useNavigate, useLocation, Link } from "react-router-dom";
3 | import ProfileInfo from "./Cards/ProfileInfo";
4 | import SearchBar from "./SearchBar/SearchBar";
5 | import { toast } from "react-hot-toast";
6 | import gsap from "gsap/all";
7 | import { FiMenu } from "react-icons/fi";
8 | import { SlideTabsExample } from "./Tabs";
9 | import { MdNightsStay } from "react-icons/md";
10 | import { IoSunnySharp } from "react-icons/io5";
11 |
12 |
13 | const Navbar = ({ userInfo, onSearchNote, handleClearSearch }) => {
14 | const [theme, setTheme] = useState("light");
15 | const [searchQuery, setSearchQuery] = useState("");
16 | const [tagQuery, setTagQuery] = useState("");
17 | const [searchType, setSearchType] = useState("text");
18 | const [isMenuOpen, setIsMenuOpen] = useState(false);
19 | const navigate = useNavigate();
20 | const location = useLocation();
21 | const logoRef = useRef(null);
22 | const searchBarRef = useRef(null);
23 | const profileRef = useRef(null);
24 | const loginButtonRef = useRef(null);
25 | const signupButtonRef = useRef(null);
26 |
27 | useEffect(() => {
28 | const savedTheme = localStorage.getItem("theme");
29 | if (savedTheme) {
30 | setTheme(savedTheme);
31 | }
32 | }, []);
33 |
34 | const toggleTheme = () => {
35 | setTheme((prevTheme) => {
36 | const newTheme = prevTheme === "light" ? "dark" : "light";
37 | localStorage.setItem("theme", newTheme);
38 | return newTheme;
39 | });
40 | };
41 |
42 | const toggleMenu = () => {
43 | setIsMenuOpen(!isMenuOpen);
44 | };
45 |
46 | // useEffect(() => {
47 | // gsap.fromTo(logoRef.current, { y: -20, opacity: 0, scale: 0.8 }, { duration: 1, y: 0, opacity: 1, scale: 1, ease: "power3.out" });
48 | // gsap.fromTo(searchBarRef.current, { x: 50, opacity: 0 }, { duration: 1, x: 0, opacity: 1, ease: "power3.out", delay: 0.5 });
49 |
50 | // if (loginButtonRef.current) {
51 | // gsap.fromTo(loginButtonRef.current, { opacity: 0, y: 20 }, { duration: 1, opacity: 1, y: 0, ease: "bounce.out", delay: 1.7 });
52 | // }
53 | // if (signupButtonRef.current) {
54 | // gsap.fromTo(signupButtonRef.current, { opacity: 0, y: 20 }, { duration: 1, opacity: 1, y: 0, ease: "bounce.out", delay: 1.5 });
55 | // }
56 | // }, []);
57 |
58 | const onLogout = () => {
59 | localStorage.clear();
60 | navigate("/login");
61 | toast.success("Logged out successfully");
62 | };
63 |
64 | const onClearSearch = () => {
65 | setSearchQuery("");
66 | setTagQuery("");
67 | handleClearSearch();
68 | };
69 |
70 | const handleSearch = () => {
71 | if (searchType === "text" && !searchQuery.trim()) {
72 | toast.error("Please enter a search term.");
73 | return;
74 | }
75 |
76 | if (searchType === "tag" && !tagQuery.trim()) {
77 | toast.error("Please enter a tag to search.");
78 | return;
79 | }
80 |
81 | onSearchNote(searchType === "text" ? searchQuery : tagQuery, searchType);
82 | };
83 |
84 | return (
85 |
90 |
91 |
92 |
93 |
94 | cribbie
95 |
96 |
97 |
98 |
99 | {/*
100 |
110 | */}
111 |
112 |
113 | {/* Hide on mobile, show on xl and up */}
114 |
115 |
116 |
117 |
118 |
119 | {/*
120 |
121 | Calendar
122 |
123 | */}
124 |
125 |
126 |
127 | Archived Notes
128 |
129 |
130 |
131 |
132 |
133 | My Profile
134 |
135 |
136 |
137 | {/*
138 |
139 |
140 |
*/}
141 |
142 | {userInfo ? (
143 |
146 | ) : (
147 |
148 | {location.pathname !== "/login" && (
149 | navigate("/login")}
152 | className="text-white bg-gray-800 border hover:bg-gray-700 transition duration-300 ease-in-out font-medium rounded-lg text-md px-4 py-1.5"
153 | >
154 | Login
155 |
156 | )}
157 |
158 | )}
159 |
160 |
161 |
162 | );
163 | };
164 |
165 | export default Navbar;
166 |
--------------------------------------------------------------------------------
/frontend/src/components/Preloader.jsx:
--------------------------------------------------------------------------------
1 | import { motion, AnimatePresence } from "framer-motion";
2 | import { useEffect, useState } from "react";
3 |
4 | const Preloader = () => {
5 | const [loading, setLoading] = useState(true);
6 | const [percentage, setPercentage] = useState(0);
7 |
8 | // Simulate loading completion and percentage increment
9 | useEffect(() => {
10 | const loadingInterval = setInterval(() => {
11 | setPercentage((prev) => {
12 | if (prev >= 100) {
13 | clearInterval(loadingInterval);
14 | setTimeout(() => setLoading(false), 800); // Wait for exit animation to complete
15 | return 100;
16 | }
17 | return prev + 1;
18 | });
19 | }, 20);
20 |
21 | return () => clearInterval(loadingInterval);
22 | }, []);
23 |
24 | return (
25 | <>
26 |
27 | {loading && (
28 |
35 | {/* Loading Percentage */}
36 |
43 | {percentage}
44 |
45 |
46 | {/* Loading Bar */}
47 |
54 |
55 | )}
56 |
57 | >
58 | );
59 | };
60 |
61 | export default Preloader;
62 |
--------------------------------------------------------------------------------
/frontend/src/components/Progressbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const ProgressBar = () => {
4 | const [scrollProgress, setScrollProgress] = useState(0);
5 |
6 | // Update progress bar on scroll
7 | useEffect(() => {
8 | const handleScroll = () => {
9 | const totalHeight =
10 | document.documentElement.scrollHeight - window.innerHeight;
11 | const scrollPosition = window.pageYOffset;
12 | const scrollPercentage = (scrollPosition / totalHeight) * 100;
13 | setScrollProgress(scrollPercentage);
14 | };
15 |
16 | window.addEventListener("scroll", handleScroll);
17 | return () => {
18 | window.removeEventListener("scroll", handleScroll);
19 | };
20 | }, []);
21 |
22 | return (
23 | <>
24 |
35 | {/* Glowing Progress Bar */}
36 |
48 |
49 | >
50 | );
51 | };
52 |
53 | export default ProgressBar;
54 |
--------------------------------------------------------------------------------
/frontend/src/components/Scroll-on-top/ScrollOnTop.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { animateScroll as scroll } from 'react-scroll';
3 |
4 | const ScrollOnTop = () => {
5 | const [showScroll, setShowScroll] = useState(false);
6 |
7 | const checkScrollTop = () => {
8 | if (!showScroll && window.pageYOffset > 400) {
9 | setShowScroll(true);
10 | } else if (showScroll && window.pageYOffset <= 400) {
11 | setShowScroll(false);
12 | }
13 | };
14 |
15 | const scrollToTop = () => {
16 | scroll.scrollToTop();
17 | };
18 |
19 | useEffect(() => {
20 | scrollToTop();
21 | }, []);
22 |
23 | useEffect(() => {
24 | window.addEventListener('scroll', checkScrollTop);
25 | return () => window.removeEventListener('scroll', checkScrollTop);
26 | });
27 |
28 | return (
29 |
30 | {
31 | showScroll &&
35 |
36 |
37 | }
38 |
39 | );
40 | };
41 |
42 | export default ScrollOnTop;
43 |
--------------------------------------------------------------------------------
/frontend/src/components/SearchBar/SearchBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { FaMagnifyingGlass } from 'react-icons/fa6';
3 | import { IoMdClose } from 'react-icons/io';
4 |
5 | const SearchBar = ({ value, tag, searchType, onChange, onTagChange, onClearSearch, handleSearch, onSearchTypeChange }) => {
6 | return (
7 |
10 | {/* Dropdown to select search type */}
11 | {/*
16 | Title
17 | Tag
18 | */}
19 |
20 | {/* Display appropriate input based on search type */}
21 | {searchType === 'text' ? (
22 |
29 | ) : (
30 |
37 | )}
38 |
39 | {/* Clear and search icons */}
40 | {value || tag ? (
41 |
45 | ) : null}
46 |
47 |
51 |
52 | );
53 | };
54 |
55 | export default SearchBar;
--------------------------------------------------------------------------------
/frontend/src/components/Tabs.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from "react";
2 | import { Link, useLocation } from "react-router-dom";
3 |
4 | // Main component that decides the theme and renders SlideTabs
5 | export const SlideTabsExample = ({ theme }) => {
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | // SlideTabs component that handles rendering the tabs
14 | const SlideTabs = ({ theme }) => {
15 | const location = useLocation();
16 | const [position, setPosition] = useState({ left: 0, width: 0, opacity: 0 });
17 | const [hoveredTab, setHoveredTab] = useState(null);
18 | const currentPathName = location.pathname;
19 |
20 | return (
21 | {
23 | setPosition((prev) => ({ ...prev, opacity: 0 }));
24 | setHoveredTab(null);
25 | }}
26 | className={`relative mx-auto flex w-fit p-1 flex-col md:flex-row gap-5 ${
27 | theme === "dark" ? "bg-black" : "bg-white"
28 | }`}
29 | >
30 | {/*
38 | Calendar
39 | */}
40 |
41 |
42 | );
43 | };
44 |
45 | // Tab component that renders individual tabs
46 | const Tab = ({
47 | children,
48 | setPosition,
49 | to,
50 | currentPathName,
51 | hoveredTab,
52 | setHoveredTab,
53 | theme,
54 | }) => {
55 | const ref = useRef(null);
56 | const isActive = currentPathName === to;
57 | const isHovered = hoveredTab === to;
58 |
59 | return (
60 | {
63 | if (!ref?.current) return;
64 | const { width } = ref.current.getBoundingClientRect();
65 | setPosition({ left: ref.current.offsetLeft, width, opacity: 1 });
66 | setHoveredTab(to);
67 | }}
68 | className={`relative z-10 mt-1.5 cursor-pointer text-xs
69 | ${isActive ? "bg-black text-white rounded-lg" : "text-black"}
70 | ${isHovered && !isActive ? "bg-gray-700 text-white rounded-lg" : ""}
71 | ${theme === "dark" && !isActive && !isHovered ? "text-white" : ""}
72 | hover:bg-gray-700 hover:text-white`}
73 | >
74 |
75 | {children}
76 |
77 |
78 | );
79 | };
80 |
81 | // Cursor component for the sliding effect
82 | const Cursor = ({ position, theme }) => {
83 | return (
84 |
94 | );
95 | };
96 |
97 | export default SlideTabsExample;
--------------------------------------------------------------------------------
/frontend/src/components/Testimonial.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import Navbar from './Navbar';
4 |
5 | const testimonialsData = [
6 | {
7 | text: `"Since I started using this notes application, my productivity has skyrocketed. The intuitive design and seamless syncing across devices make it an essential tool for organizing my thoughts and tasks. The powerful search functionality ensures I can always find what I need, and the ability to collaborate with others has transformed the way I work on projects. It’s truly a game-changer for anyone looking to stay organized and efficient."`,
8 | name: "Leroy Jenkins",
9 | position: "CTO of Company Co.",
10 | img: "https://i.pinimg.com/736x/a8/9f/67/a89f67343169f2a76369d2df3b364875.jpg",
11 | },
12 | {
13 | text: `"With this notes app, staying organized is a breeze. Its user-friendly interface and powerful features make it an essential tool. Perfect for managing tasks and capturing ideas on the go. Highly recommended!"`,
14 | name: "Kendrick Lamar",
15 | position: "CEO of Oklama",
16 | img: "https://i.pinimg.com/736x/a8/9f/67/a89f67343169f2a76369d2df3b364875.jpg",
17 | },
18 | {
19 | text: `"This notes application is a game-changer. It simplifies task management and boosts productivity. Its intuitive design and robust features make organizing notes effortless, ensuring I never miss a detail. Highly recommended!"`,
20 | name: "Drake",
21 | position: "CTO of OVO Company",
22 | img: "https://i.pinimg.com/736x/a8/9f/67/a89f67343169f2a76369d2df3b364875.jpg",
23 | },
24 | {
25 | text: `"I've tried many note-taking apps, but this one stands out for its simplicity and functionality. The ability to categorize and tag notes makes retrieval a breeze, and the collaborative features have been invaluable for team projects. The app’s reliability and ease of use have made it an essential part of my daily routine, helping me stay on top of my tasks and ideas effortlessly."`,
26 | name: "Martin Garrix",
27 | position: "CEO of Spotify",
28 | img: "https://i.pinimg.com/736x/a8/9f/67/a89f67343169f2a76369d2df3b364875.jpg",
29 | }
30 | ];
31 |
32 | const Testimonial = () => {
33 | const user = JSON.parse(localStorage.getItem("user"));
34 | const [currentIndex, setCurrentIndex] = useState(0);
35 | const location = useLocation();
36 |
37 | const testimonialDisplayDuration = 5000;
38 |
39 | useEffect(() => {
40 | const interval = setInterval(() => {
41 | setCurrentIndex((prevIndex) => (prevIndex + 1) % testimonialsData.length);
42 | }, testimonialDisplayDuration);
43 | return () => clearInterval(interval);
44 | }, []);
45 |
46 | return (
47 | <>
48 | {location.pathname === '/testimonial' && }
49 |
50 |
51 |
52 | {/* Left Side - Title */}
53 |
54 |
What our customers think
55 |
56 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Minus commodi sint, similique cupiditate possimus suscipit delectus illum eos iure magnam!
57 |
58 | {/* Progress Bar */}
59 |
65 |
66 |
67 | {/* Right Side - Testimonials */}
68 |
69 | {testimonialsData.map((testimonial, index) => (
70 |
74 |
75 |
{testimonial.text}
76 |
77 |
82 |
83 |
{testimonial.name}
84 |
{testimonial.position}
85 |
86 |
87 |
88 |
89 | ))}
90 |
91 |
92 |
93 |
94 | >
95 |
96 | );
97 | };
98 |
99 | export default Testimonial;
100 |
--------------------------------------------------------------------------------
/frontend/src/components/Toggle.jsx:
--------------------------------------------------------------------------------
1 | import { motion } from "framer-motion";
2 | import { useState } from "react";
3 | import { FiMoon, FiSun } from "react-icons/fi";
4 |
5 | const TOGGLE_CLASSES =
6 | "text-sm font-medium flex items-center gap-2 px-3 md:pl-3 md:pr-3.5 py-3 md:py-1.5 transition-colors relative z-10";
7 |
8 | const Example = () => {
9 | const [selected, setSelected] = useState("light");
10 | return (
11 |
16 |
17 |
18 | );
19 | };
20 |
21 | const SliderToggle = ({ selected, setSelected }) => {
22 | return (
23 |
24 |
{
29 | setSelected("light");
30 | }}
31 | >
32 |
33 | Light
34 |
35 |
{
40 | setSelected("dark");
41 | }}
42 | >
43 |
44 | Dark
45 |
46 |
51 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default Example;
--------------------------------------------------------------------------------
/frontend/src/components/sticky_footer/Content.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useLocation } from "react-router-dom";
3 | import { FaArrowUp } from "react-icons/fa";
4 |
5 | const navLinks = [
6 | { link: "/about", name: "About Us" },
7 | { link: "/contact", name: "Contact Us" },
8 | { link: "/feedback", name: "Feedback" },
9 | { link: "/support", name: "Help & Support" },
10 | {
11 | link: "https://github.com/yashmandi/notes-app",
12 | name: "GitHub",
13 | external: true,
14 | },
15 | { link: "/app-version", name: "App Version" },
16 | ];
17 |
18 | export default function Content() {
19 | return (
20 |
21 |
22 | {navLinks.map(({ link, name, external }, index) => (
23 |
24 | ))}
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
32 | const NavLink = ({ link, name, external }) => (
33 |
39 | {name}
40 |
41 | );
42 |
43 | const Footer = () => {
44 | const [isWide, setIsWide] = useState(window.innerWidth > 640);
45 | const location = useLocation();
46 | const [showButton, setshowButton] = useState(false);
47 |
48 | //check if the current page is the landing page
49 | useEffect(() => {
50 | if (location.pathname === "/") {
51 | setshowButton(true);
52 | } else {
53 | setshowButton(false);
54 | }
55 | }, [location.pathname]);
56 |
57 | // Scroll to top function
58 | const scrollToTop = () => {
59 | window.scrollTo({ top: 0, behavior: "smooth" });
60 | };
61 |
62 | useEffect(() => {
63 | const handleResize = () => setIsWide(window.innerWidth > 640);
64 | window.addEventListener("resize", handleResize);
65 | return () => window.removeEventListener("resize", handleResize);
66 | }, []);
67 |
68 | return (
69 |
70 | {!isWide && (
71 |
72 |
77 |
78 | )}
79 |
84 |
89 | Scribbie {!isWide && }
90 |
91 |
©2024 by Scribbie
92 | {showButton && (
93 |
97 |
98 |
99 | )}
100 |
101 |
102 | );
103 | };
104 |
105 | const Nav = () => {
106 | return (
107 |
108 |
109 |
110 | Unlocking creativity and productivity through intuitive note-taking
111 | and seamless organization.
112 |
113 |
114 |
115 | );
116 | };
117 |
--------------------------------------------------------------------------------
/frontend/src/components/sticky_footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Content from "./Content";
3 |
4 | const Footer = () => {
5 | return (
6 |
16 | );
17 | };
18 |
19 | export default Footer;
20 |
--------------------------------------------------------------------------------
/frontend/src/hooks/noteActions.jsx:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../utils/axiosInstance";
2 | import toast from "react-hot-toast";
3 |
4 | const apiBaseUrl = import.meta.env.VITE_BACKEND_URL;
5 |
6 | const noteActions = (getAllNotes, onClose) => {
7 |
8 | // Helper function to handle form data construction
9 | const createFormData = (noteData) => {
10 | const formData = new FormData();
11 | formData.append("title", noteData.title);
12 | formData.append("content", noteData.content);
13 | formData.append("background", noteData.background);
14 | formData.append("isPinned", noteData.isPinned);
15 |
16 | // Append tags and media files only if they exist
17 | noteData.tags?.forEach(tag => formData.append("tags[]", tag));
18 | noteData.attachments?.forEach(file => formData.append("attachments", file));
19 | noteData.photos?.forEach(photo => formData.append("photos", photo));
20 | noteData.videos?.forEach(video => formData.append("videos", video));
21 |
22 | return formData;
23 | };
24 |
25 | // Handle adding a new note
26 | const addNewNote = async (noteData) => {
27 | try {
28 | const formData = createFormData(noteData);
29 |
30 | const response = await axiosInstance.post(`${apiBaseUrl}/add-note`, formData, {
31 | headers: { "Content-Type": "multipart/form-data" },
32 | });
33 |
34 | if (response.data?.note) {
35 | getAllNotes();
36 | onClose();
37 | toast.success("Note added successfully");
38 | console.log(response.data);
39 | }
40 | } catch (err) {
41 | console.log(err, "Failed to add a note");
42 | }
43 | };
44 |
45 | // Handle editing an existing note
46 | const editNote = async (noteData) => {
47 |
48 | try {
49 | if (!noteData._id) {
50 | console.log("Invalid note data.");
51 | return;
52 | }
53 |
54 | const formData = createFormData(noteData);
55 |
56 | const response = await axiosInstance.put(
57 | `/edit-note/${noteData._id}`,
58 | formData,
59 | {
60 | headers: { "Content-Type": "multipart/form-data" },
61 | }
62 | );
63 |
64 | if (response.data?.note) {
65 | getAllNotes();
66 | onClose();
67 | toast.success("Note updated successfully");
68 | }
69 | } catch (err) {
70 | console.log(err, "Failed to update a note");
71 | }
72 | };
73 |
74 | // Handle deleting a note
75 | const deleteNote = async (noteId) => {
76 | try {
77 | const response = await axiosInstance.delete(
78 | `${apiBaseUrl}/delete-note/${noteId}`
79 | );
80 |
81 | if (response.data?.success) {
82 | getAllNotes();
83 | toast.success("Note deleted successfully");
84 | }
85 | } catch (err) {
86 | console.log(err, "Failed to delete the note");
87 | }
88 | };
89 |
90 | // Handle toggling the pinned status
91 | const togglePinnedStatus = async (noteId, currentStatus) => {
92 | try {
93 | const response = await axiosInstance.put(
94 | `${apiBaseUrl}/toggle-pin/${noteId}`,
95 | { isPinned: !currentStatus }
96 | );
97 |
98 | if (response.data?.note) {
99 | getAllNotes();
100 | toast.success("Pinned status updated successfully");
101 | }
102 | } catch (err) {
103 | console.log(err, "Failed to update pinned status");
104 | }
105 | };
106 |
107 | return {
108 | addNewNote,
109 | editNote,
110 | deleteNote,
111 | togglePinnedStatus,
112 | };
113 | };
114 |
115 | export default noteActions;
116 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | @layer base {
8 | html {
9 | font-family: "Poppins", sans-serif;
10 | /* scroll-behavior: smooth; */
11 | }
12 |
13 | body {
14 | background-color: #fdfeff;
15 | overflow-x: hidden;
16 | scroll-behavior: smooth;
17 | }
18 | }
19 |
20 | @layer components {
21 | .input-box {
22 | @apply w-full text-sm bg-transparent border-[1.5px] px-5 py-3 rounded mb-4 outline-none;
23 | }
24 |
25 | .btn-primary {
26 | @apply w-full text-sm bg-primary text-white p-2 rounded my-1 hover:bg-blue-600;
27 | }
28 |
29 | .icon-btn {
30 | @apply text-xl text-slate-300 cursor-pointer hover:text-primary;
31 | }
32 |
33 | .input-label {
34 | @apply text-xs text-slate-400;
35 | }
36 | }
37 |
38 | @keyframes spin {
39 | 0% {
40 | transform: rotate(0deg);
41 | }
42 |
43 | 100% {
44 | transform: rotate(360deg);
45 | }
46 | }
47 |
48 | .loader {
49 | border-top-color: gray;
50 | animation: spin 1s linear infinite;
51 | }
52 |
53 | .animated-background {
54 | background-size: 400%;
55 |
56 | -webkit-animation: animation 3s ease infinite;
57 | -moz-animation: animation 3s ease infinite;
58 | animation: animation 3s ease infinite;
59 | }
60 |
61 | @keyframes animation {
62 |
63 | 0%,
64 | 100% {
65 | background-position: 0% 50%;
66 | }
67 |
68 | 50% {
69 | background-position: 100% 50%;
70 | }
71 | }
72 |
73 | ::-webkit-scrollbar {
74 | width: 7px;
75 | }
76 |
77 | /* Track */
78 | ::-webkit-scrollbar-track {
79 | background: #e3e3e6;
80 | }
81 |
82 | /* Handle */
83 | ::-webkit-scrollbar-thumb {
84 | background: #999999;
85 | border-radius: 12px;
86 | }
87 |
88 | /* Handle on hover */
89 | ::-webkit-scrollbar-thumb:hover {
90 | background: #a19d9d;
91 | border-radius: 12px;
92 | }
93 |
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 | import { Toaster } from "react-hot-toast";
6 | import { GoogleOAuthProvider } from "@react-oauth/google";
7 |
8 | createRoot(document.getElementById("root")).render(
9 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 |
--------------------------------------------------------------------------------
/frontend/src/pages/ForgotPassword/VerifyEmail.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Navbar from "../../components/Navbar";
3 | import { Link, useNavigate } from "react-router-dom";
4 | import { validateEmail } from "../../utils/helper";
5 | import axiosInstance from "../../utils/axiosInstance";
6 | import toast from "react-hot-toast";
7 | import { FaEye, FaEyeSlash } from "react-icons/fa";
8 | import { GoogleLogin } from "@react-oauth/google";
9 | import CircularLoader from "../../components/CircularLoader";
10 |
11 | const VerifyEmail = ({ setUser }) => {
12 | const [email, setEmail] = useState("");
13 | const [password, setPassword] = useState("");
14 | const [showPassword, setShowPassword] = useState(false);
15 | const [loading, setLoading] = useState(false);
16 | const [error, setError] = useState(null);
17 |
18 | const navigate = useNavigate();
19 |
20 | const responseMsg = async (response) => {
21 | try {
22 | setLoading(true);
23 | const token = response.credential;
24 | const res = await axiosInstance.post("/google-auth", { token });
25 |
26 | if (res.data && res.data.accessToken) {
27 | localStorage.setItem("token", res.data.accessToken);
28 | localStorage.setItem("user", JSON.stringify(res.data.user));
29 | navigate("/dashboard");
30 | toast.success("Signed in successfully", {
31 | style: {
32 | fontSize: "13px",
33 | maxWidth: "400px",
34 | boxShadow: "4px 4px 8px rgba(0, 1, 4, 0.1)",
35 | borderRadius: "8px",
36 | borderColor: "rgba(0, 0, 0, 0.8)",
37 | marginTop: "60px",
38 | marginRight: "10px",
39 | },
40 | });
41 | } else {
42 | toast.error("Failed to sign in with Google", {
43 | style: {
44 | fontSize: "13px",
45 | maxWidth: "400px",
46 | boxShadow: "4px 4px 8px rgba(0, 1, 4, 0.1)",
47 | borderRadius: "8px",
48 | borderColor: "rgba(0, 0, 0, 0.8)",
49 | marginTop: "60px",
50 | marginRight: "10px",
51 | },
52 | });
53 | }
54 | } catch (error) {
55 | console.log("Error response:", error.response);
56 | toast.error("Failed to sign in with Google", {
57 | style: {
58 | fontSize: "13px",
59 | maxWidth: "400px",
60 | boxShadow: "4px 4px 8px rgba(0, 1, 4, 0.1)",
61 | borderRadius: "8px",
62 | borderColor: "rgba(0, 0, 0, 0.8)",
63 | marginTop: "60px",
64 | marginRight: "10px",
65 | },
66 | });
67 | } finally {
68 | setLoading(false);
69 | }
70 | };
71 |
72 | const errorMsg = (error) => {
73 | console.log(error);
74 | };
75 |
76 | const handleOtpSend = async (e) => {
77 | e.preventDefault();
78 |
79 | if (!validateEmail(email)) {
80 | toast.error("Please enter a valid email", {
81 | style: {
82 | fontSize: "13px",
83 | maxWidth: "400px",
84 | boxShadow: "px 4px 8px rgba(0, 1, 4, 0.1)",
85 | borderRadius: "8px",
86 | borderColor: "rgba(0, 0, 0, 0.8)",
87 | marginRight: "10px",
88 | },
89 | });
90 | return;
91 | }
92 |
93 | setError(null);
94 |
95 | try {
96 | const response = await axiosInstance.post("http://localhost:5000/verify-email", {
97 | email
98 | });
99 |
100 | if (response.data) {
101 | console.log(response)
102 | const id = response.data.id
103 | navigate(`/verify-otp/${id}`);
104 | toast.success("Otp Sent successfully", {
105 | style: {
106 | fontSize: "13px",
107 | maxWidth: "400px",
108 | boxShadow: "px 4px 8px rgba(0, 1, 4, 0.1)",
109 | borderRadius: "8px",
110 | borderColor: "rgba(0, 0, 0, 0.8)",
111 | marginRight: "10px",
112 | },
113 | });
114 | } else {
115 | toast.error("Login failed, please try again", {
116 | style: {
117 | fontSize: "13px",
118 | maxWidth: "400px",
119 | boxShadow: "px 4px 8px rgba(0, 1, 4, 0.1)",
120 | borderRadius: "8px",
121 | borderColor: "rgba(0, 0, 0, 0.8)",
122 | marginRight: "10px",
123 | },
124 | });
125 | }
126 | } catch (error) {
127 | if (
128 | error.response &&
129 | error.response.data &&
130 | error.response.data.message
131 | ) {
132 | toast.error(error.response.data.message, {
133 | style: {
134 | fontSize: "13px",
135 | maxWidth: "400px",
136 | boxShadow: "px 4px 8px rgba(0, 1, 4, 0.1)",
137 | borderRadius: "8px",
138 | borderColor: "rgba(0, 0, 0, 0.8)",
139 | marginRight: "10px",
140 | },
141 | });
142 | } else {
143 | setError("Something went wrong. Please try again later.");
144 | }
145 | }
146 | };
147 |
148 | const togglePasswordVisibility = () => {
149 | setShowPassword(!showPassword);
150 | };
151 |
152 | return (
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
166 |
167 |
168 |
169 | Home
170 |
171 |
172 |
173 | Welcome Back!
174 |
175 |
176 |
177 | Smart note-taking for smarter work and better results. Stay
178 | organized, stay inspired, and stay ahead.
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 | Welcome Back!
187 |
188 |
189 |
190 | Smart note-taking for smarter work and better results. Stay
191 | organized, stay inspired, and stay ahead.
192 |
193 |
194 |
195 |
199 |
200 |
204 | Enter your Email id to reset password
205 |
206 | setEmail(e.target.value)}
212 | className="mt-1 p-2 w-full rounded-md border border-gray-100 bg-white text-sm text-gray-700 shadow-sm"
213 | />
214 |
215 |
216 |
217 |
221 | {loading ? (
222 |
223 | sending otp
224 |
225 | ) : (
226 | "Send OTP"
227 | )}
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 | );
238 | };
239 |
240 | export default VerifyEmail;
241 |
--------------------------------------------------------------------------------
/frontend/src/pages/ForgotPassword/VerifyOtp.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Navbar from "../../components/Navbar";
3 | import { Link, useNavigate, useParams } from "react-router-dom";
4 | import axiosInstance from "../../utils/axiosInstance";
5 | import toast from "react-hot-toast";
6 | import CircularLoader from "../../components/CircularLoader";
7 |
8 | const VerifyOtp = ({ setUser }) => {
9 | const [otp, setOtp] = useState("");
10 | const [loading, setLoading] = useState(false);
11 |
12 | const navigate = useNavigate();
13 |
14 | const {id} = useParams()
15 |
16 | const handleOtpSend = async (e) => {
17 | e.preventDefault();
18 |
19 | console.log(id);
20 |
21 | try {
22 | const response = await axiosInstance.post("http://localhost:5000/verify-otp", {
23 | id: id,
24 | otp: otp
25 | });
26 |
27 | if (response.data) {
28 | navigate(`/reset-password/${id}`);
29 | toast.success("Otp verified", {
30 | style: {
31 | fontSize: "13px",
32 | maxWidth: "400px",
33 | boxShadow: "px 4px 8px rgba(0, 1, 4, 0.1)",
34 | borderRadius: "8px",
35 | borderColor: "rgba(0, 0, 0, 0.8)",
36 | marginRight: "10px",
37 | },
38 | });
39 | } else {
40 | toast.error("Login failed, please try again", {
41 | style: {
42 | fontSize: "13px",
43 | maxWidth: "400px",
44 | boxShadow: "px 4px 8px rgba(0, 1, 4, 0.1)",
45 | borderRadius: "8px",
46 | borderColor: "rgba(0, 0, 0, 0.8)",
47 | marginRight: "10px",
48 | },
49 | });
50 | }
51 | } catch (error) {
52 | if (
53 | error.response &&
54 | error.response.data &&
55 | error.response.data.message
56 | ) {
57 | toast.error(error.response.data.message, {
58 | style: {
59 | fontSize: "13px",
60 | maxWidth: "400px",
61 | boxShadow: "px 4px 8px rgba(0, 1, 4, 0.1)",
62 | borderRadius: "8px",
63 | borderColor: "rgba(0, 0, 0, 0.8)",
64 | marginRight: "10px",
65 | },
66 | });
67 | }
68 | }
69 | };
70 |
71 | return (
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
85 |
86 |
87 |
88 | Home
89 |
90 |
91 |
92 | Welcome Back!
93 |
94 |
95 |
96 | Smart note-taking for smarter work and better results. Stay
97 | organized, stay inspired, and stay ahead.
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Welcome Back!
106 |
107 |
108 |
109 | Smart note-taking for smarter work and better results. Stay
110 | organized, stay inspired, and stay ahead.
111 |
112 |
113 |
114 |
118 |
119 |
123 | Enter OTP received on your mail id
124 |
125 | setOtp(e.target.value)}
131 | className="mt-1 p-2 w-full rounded-md border border-gray-100 bg-white text-sm text-gray-700 shadow-sm"
132 | />
133 |
134 |
135 |
136 |
140 | {loading ? (
141 |
142 | Verifying OTP
143 |
144 | ) : (
145 | "Verify OTP"
146 | )}
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | );
157 | };
158 |
159 | export default VerifyOtp;
160 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home/AddEditNotes.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import TagInput from "../../components/Input/TagInput";
3 | import { MdClose } from "react-icons/md";
4 | import toast from "react-hot-toast";
5 | import AddAttachmentsInput from "../../components/Input/AddAttachmentInput";
6 | import ReactQuill from "react-quill";
7 | import "react-quill/dist/quill.snow.css";
8 | import { FaMicrophone, FaMicrophoneSlash } from "react-icons/fa";
9 | import noteActions from "../../hooks/noteActions"; // Import the hook
10 |
11 | const AddEditNotes = ({ noteData, type, getAllNotes, onClose }) => {
12 | const [title, setTitle] = useState("");
13 | const [content, setContent] = useState("");
14 | const [tags, setTags] = useState([]);
15 | const [attachments, setAttachments] = useState([]);
16 | const [background, setBackground] = useState("#ffffff");
17 | const [error, setError] = useState(null);
18 | const [photos, setPhotos] = useState([]);
19 | const [videos, setVideos] = useState([]);
20 | const [isListening, setIsListening] = useState(false);
21 | const [isPinned, setIsPinned] = useState(false);
22 | const [isSpeechSupported, setIsSpeechSupported] = useState(true);
23 | const [initialData, setInitialData] = useState(null);
24 | const predefinedColors = ["#f4f4f4", "#ffcccc", "#ffeeba", "#fff4c2", "#d4f8d4", "#c8f6f4"];
25 |
26 | const MAX_TITLE_LENGTH = 60;
27 | const MAX_CONTENT_LENGTH = 2500;
28 |
29 | const SpeechRecognition =
30 | window.SpeechRecognition || window.webkitSpeechRecognition;
31 | const recognition = SpeechRecognition ? new SpeechRecognition() : null;
32 | const { addNewNote, editNote } = noteActions(getAllNotes, onClose);
33 |
34 | useEffect(() => {
35 | if (!SpeechRecognition) {
36 | setIsSpeechSupported(false);
37 | }
38 | }, []);
39 |
40 | if (recognition) {
41 | recognition.continuous = true;
42 | recognition.interimResults = true;
43 |
44 | recognition.onresult = (event) => {
45 | let isFinal = event.results[event.results.length - 1].isFinal;
46 | let transcript = event.results[event.results.length - 1][0].transcript;
47 |
48 | if (isFinal) {
49 | setContent((prevContent) =>
50 | (prevContent.trim() + " " + transcript).trim()
51 | );
52 | }
53 | };
54 |
55 | recognition.onerror = (event) => {
56 | console.error("Speech recognition error", event.error);
57 | let errorMessage = "Speech recognition failed";
58 |
59 | switch (event.error) {
60 | case "no-speech":
61 | errorMessage = "No speech was detected. Please try again.";
62 | break;
63 | case "audio-capture":
64 | errorMessage = "Audio capture failed. Please check your microphone.";
65 | break;
66 | case "not-allowed":
67 | errorMessage = "Permission to use microphone was denied.";
68 | break;
69 | case "service-not-allowed":
70 | errorMessage = "Speech service is not allowed.";
71 | break;
72 | default:
73 | errorMessage = "An unknown error occurred.";
74 | }
75 |
76 | setError(errorMessage);
77 | toast.error(errorMessage);
78 | };
79 | }
80 |
81 | useEffect(() => {
82 | if (noteData) {
83 | // Set initial data state for revert functionality
84 | setInitialData({ ...noteData });
85 |
86 | // Set form data
87 | setTitle(noteData.title);
88 | setContent(noteData.content);
89 | setTags(noteData.tags);
90 | setAttachments(noteData.files || []);
91 | setBackground(noteData.background || "#ffffff");
92 | setPhotos(noteData.photos || []);
93 | setVideos(noteData.videos || []);
94 | setIsPinned(noteData.isPinned || false);
95 | }
96 | }, [type, noteData]);
97 |
98 | const toggleListening = () => {
99 | if (!isSpeechSupported) {
100 | toast.error("Speech recognition is not supported by your browser.");
101 | return;
102 | }
103 | if (isListening) {
104 | recognition.stop();
105 | setIsListening(false);
106 | } else {
107 | recognition.start();
108 | setIsListening(true);
109 | }
110 | };
111 |
112 | const handleFileUpload = (files) => {
113 | setAttachments((prevAttachments) => [...prevAttachments, ...files]);
114 | };
115 |
116 | const handlePhotoUpload = (files) => {
117 | setPhotos((prevPhotos) => [...prevPhotos, ...files]);
118 | };
119 |
120 | const handleVideoUpload = (files) => {
121 | setVideos((prevVideos) => [...prevVideos, ...files]);
122 | };
123 |
124 | const handleTitleChange = (e) => {
125 | const newTitle = e.target.value;
126 | if (newTitle.length <= MAX_TITLE_LENGTH) {
127 | setTitle(newTitle);
128 | }
129 | };
130 |
131 | const handleContentChange = (value) => {
132 | if (value.length <= MAX_CONTENT_LENGTH) {
133 | setContent(value);
134 | }
135 | };
136 |
137 | // Custom toolbar configuration
138 | const customModules = {
139 | toolbar: [
140 | [{ header: [1, 2, false] }],
141 | ["bold", "italic", "underline"],
142 | [{ list: "ordered" }, { list: "bullet" }],
143 | ["code-block", "link"], // Markdown-like options (code-block, link)
144 | [{ 'align': [] }],
145 | ["clean"],
146 | [{ 'indent': '-1' }, { 'indent': '+1' }], // Indentation options (useful for blockquotes)
147 | ],
148 | clipboard: {
149 | matchVisual: false,
150 | },
151 | };
152 |
153 | const handleBackgroundChange = (e) => {
154 | setBackground(e.target.value);
155 | };
156 |
157 | const handleSaveNote = () => {
158 | if (!title) {
159 | setError("Title is required");
160 | return;
161 | }
162 | if (!content) {
163 | setError("Content is required");
164 | return;
165 | }
166 |
167 | if (type === "edit") {
168 | editNote({ _id: noteData._id, title, content, tags, attachments, photos, videos, background, isPinned });
169 | } else {
170 | addNewNote({ title, content, tags, attachments, photos, videos, background, isPinned });
171 | }
172 | };
173 |
174 | // const handleRevertChanges = () => {
175 | // if (!initialData) return;
176 |
177 | // // Revert all form fields to initial data values
178 | // setTitle(initialData.title);
179 | // setContent(initialData.content);
180 | // setTags(initialData.tags);
181 | // setAttachments(initialData.files || []);
182 | // setBackground(initialData.background || "#ffffff");
183 | // setPhotos(initialData.photos || []);
184 | // setVideos(initialData.videos || []);
185 | // setIsPinned(initialData.isPinned || false);
186 | // };
187 |
188 | return (
189 |
190 | {/*
194 |
195 | */}
196 |
197 |
198 |
Title
199 |
200 |
207 |
208 | {title.length}/{MAX_TITLE_LENGTH}
209 |
210 |
211 |
212 |
213 |
214 |
Content
215 |
216 |
217 |
226 |
227 | {content.length}/{MAX_CONTENT_LENGTH}
228 |
229 | {/*
234 | {isListening ? : }
235 | */}
236 | {!isSpeechSupported && (
237 |
238 | Speech recognition is not supported in this browser.
239 |
240 | )}
241 |
242 |
243 |
244 |
245 |
246 |
Background Color
247 |
248 | {predefinedColors.map((color) => (
249 | setBackground(color)}
252 | // className="border border-2"
253 | style={{
254 | marginTop: "8px",
255 | backgroundColor: color,
256 | width: "36px",
257 | height: "36px",
258 | borderRadius: "50%",
259 | border: background === color ? "2px solid black" : "1px solid gray",
260 | cursor: "pointer",
261 | }}
262 | >
263 | ))}
264 |
265 |
266 |
267 |
268 |
269 | Tags
270 |
271 |
272 |
273 |
274 |
Add Attachments
275 |
276 |
277 |
278 |
279 |
Add Photos
280 |
284 |
285 |
286 |
287 |
Add Videos
288 |
292 |
293 |
294 | {error &&
{error}
}
295 |
296 |
297 |
298 | Close
299 |
300 |
304 | {type === "edit" ? "Update Note" : "Add Note"}
305 |
306 |
307 |
308 | {/* Revert Button */}
309 | {/* {initialData && (
310 |
314 | Revert Changes
315 |
316 | )} */}
317 |
318 | );
319 | };
320 |
321 | export default AddEditNotes;
--------------------------------------------------------------------------------
/frontend/src/pages/Home/Templates.jsx:
--------------------------------------------------------------------------------
1 | export const templates = {
2 | meeting: {
3 | title: "Meeting Notes",
4 | content: `
5 | Date: [Enter date]
6 | Attendees: [Enter attendees]
7 |
8 | Agenda:
9 |
10 | [Enter agenda item]
11 | [Enter agenda item]
12 |
13 |
14 | Notes:
15 |
16 | [Enter meeting note]
17 |
18 |
19 | Action Items:
20 |
21 | [Enter action item with owner and due date]
22 |
23 | `,
24 | tags: ["meeting", "notes"]
25 | },
26 | todo: {
27 | title: "To-Do List",
28 | content: `
29 | Tasks:
30 |
35 |
36 | Notes:
37 |
38 | [Enter any additional notes]
39 |
40 | `,
41 | tags: ["to-do", "list"]
42 | },
43 | brainstorming: {
44 | title: "Brainstorming Notes",
45 | content: `
46 | Topic: [Enter topic]
47 |
48 | Ideas:
49 |
50 | [Enter idea 1]
51 | [Enter idea 2]
52 | [Enter idea 3]
53 |
54 |
55 | Next Steps:
56 |
57 | [Enter next step or action]
58 |
59 | `,
60 | tags: ["brainstorming", "ideas"]
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/frontend/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const { createProxyMiddleware } = require('http-proxy-middleware');
2 | const apiBaseUrl = import.meta.env.VITE_BACKEND_URL;
3 |
4 | module.exports = function (app) {
5 | app.use(
6 | '/api',
7 | createProxyMiddleware({
8 | target: `${apiBaseUrl}`,
9 | changeOrigin: true
10 | })
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/frontend/src/utils/ProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Navigate } from "react-router-dom";
3 |
4 | const ProtectedRoute = ({ childeren }) => {
5 | const token = localStorage.getItem("token");
6 |
7 | if (!token) {
8 | return
9 | }
10 |
11 | return childeren;
12 | }
13 |
14 | export default ProtectedRoute
--------------------------------------------------------------------------------
/frontend/src/utils/axiosInstance.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | const apiBaseUrl = import.meta.env.VITE_BACKEND_URL;
3 |
4 |
5 | const axiosInstance = axios.create({
6 | baseURL:`${apiBaseUrl}`,
7 | timeout: 10000,
8 | headers: { "Content-Type": "application/json" },
9 | });
10 |
11 | axiosInstance.interceptors.request.use(
12 | (config) => {
13 | const accessToken = localStorage.getItem("token");
14 | if (accessToken) {
15 | config.headers.Authorization = `Bearer ${accessToken}`;
16 | }
17 |
18 | return config;
19 | },
20 | (error) => {
21 | return Promise.reject(error);
22 | }
23 | );
24 |
25 | export default axiosInstance;
26 |
--------------------------------------------------------------------------------
/frontend/src/utils/helper.js:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | export const validateEmail = (email) => {
4 | if (!email || email.trim() === "") {
5 | return { valid: false, error: "Email is required" };
6 | }
7 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
8 | if (!emailRegex.test(email) || (email[0] >= "1" && email[0] <= "9")) {
9 | return { valid: false, error: "Invalid Email format" };
10 | }
11 |
12 | return { valid: true };
13 | };
14 |
15 | export const validateName = (name) => {
16 | if (!name || name.trim() === "") {
17 | return { valid: false, error: "Name is required" };
18 | }
19 | const nameRegex = /^[A-Za-z]+(?: [A-Za-z]+)*$/;
20 | if (!nameRegex.test(name) || (name[0] >= "1" && name[0] <= "9")) {
21 | return { valid: false, error: "Invalid Name format" };
22 | }
23 |
24 | return { valid: true };
25 | };
26 |
27 | export const validatePassword = (password) => {
28 | if (!password || password.trim() === "") {
29 | return { valid: false, error: "Password is required" };
30 | }
31 | if (!/[A-Z]/.test(password)) {
32 | return {
33 | valid: false,
34 | error: "Password must include atleast one Uppercase letter",
35 | };
36 | }
37 | if (!/[a-z]/.test(password)) {
38 | return {
39 | valid: false,
40 | error: "Password must include atleast one Lower letter",
41 | };
42 | }
43 | if (!/[!"#$%&'()*+,-.:;<=>?@[\]^_`{|}~]/.test(password)) {
44 | return {
45 | valid: false,
46 | error: "Password must include atleast one special character",
47 | };
48 | }
49 | if (!(password.length >= 8)) {
50 | return { valid: false, error: "Min password length should be 8" };
51 | }
52 |
53 | return { valid: true };
54 | };
55 |
56 | export const getInitials = (name) => {
57 | if (!name) return "";
58 |
59 | const words = name.trim().split(" ");
60 | let initials = "";
61 |
62 | if (words.length > 0) {
63 | initials += words[0][0]; // First initial
64 | }
65 |
66 | if (words.length > 1) {
67 | initials += words[1][0]; // Second initial, if available
68 | }
69 |
70 | return initials.toUpperCase();
71 | };
72 |
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | colors: {
10 | primary: "#2b85ff",
11 | secondary: "#ef863e",
12 | },
13 | zIndex: {
14 | '-1': '-1',
15 | '-10': '-10',
16 | '-20': '-20',
17 | },
18 | },
19 | },
20 | plugins: [
21 | ],
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig, loadEnv } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | export default defineConfig(({ mode }) => {
5 | const env = loadEnv(mode, process.cwd())
6 |
7 | return {
8 | plugins: [react()],
9 | define: {
10 | 'import.meta.env': env,
11 | },
12 | }
13 | })
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notes-app",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "gsap": "^3.12.5",
9 | "react-modal": "^3.16.1"
10 | }
11 | },
12 | "node_modules/exenv": {
13 | "version": "1.2.2",
14 | "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
15 | "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw=="
16 | },
17 | "node_modules/gsap": {
18 | "version": "3.12.5",
19 | "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.5.tgz",
20 | "integrity": "sha512-srBfnk4n+Oe/ZnMIOXt3gT605BX9x5+rh/prT2F1SsNJsU1XuMiP0E2aptW481OnonOGACZWBqseH5Z7csHxhQ==",
21 | "license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
22 | },
23 | "node_modules/js-tokens": {
24 | "version": "4.0.0",
25 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
26 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
27 | },
28 | "node_modules/loose-envify": {
29 | "version": "1.4.0",
30 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
31 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
32 | "dependencies": {
33 | "js-tokens": "^3.0.0 || ^4.0.0"
34 | },
35 | "bin": {
36 | "loose-envify": "cli.js"
37 | }
38 | },
39 | "node_modules/object-assign": {
40 | "version": "4.1.1",
41 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
42 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
43 | "engines": {
44 | "node": ">=0.10.0"
45 | }
46 | },
47 | "node_modules/prop-types": {
48 | "version": "15.8.1",
49 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
50 | "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
51 | "dependencies": {
52 | "loose-envify": "^1.4.0",
53 | "object-assign": "^4.1.1",
54 | "react-is": "^16.13.1"
55 | }
56 | },
57 | "node_modules/react": {
58 | "version": "18.3.1",
59 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
60 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
61 | "peer": true,
62 | "dependencies": {
63 | "loose-envify": "^1.1.0"
64 | },
65 | "engines": {
66 | "node": ">=0.10.0"
67 | }
68 | },
69 | "node_modules/react-dom": {
70 | "version": "18.3.1",
71 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
72 | "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
73 | "peer": true,
74 | "dependencies": {
75 | "loose-envify": "^1.1.0",
76 | "scheduler": "^0.23.2"
77 | },
78 | "peerDependencies": {
79 | "react": "^18.3.1"
80 | }
81 | },
82 | "node_modules/react-is": {
83 | "version": "16.13.1",
84 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
85 | "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
86 | },
87 | "node_modules/react-lifecycles-compat": {
88 | "version": "3.0.4",
89 | "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
90 | "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
91 | },
92 | "node_modules/react-modal": {
93 | "version": "3.16.1",
94 | "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz",
95 | "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==",
96 | "dependencies": {
97 | "exenv": "^1.2.0",
98 | "prop-types": "^15.7.2",
99 | "react-lifecycles-compat": "^3.0.0",
100 | "warning": "^4.0.3"
101 | },
102 | "engines": {
103 | "node": ">=8"
104 | },
105 | "peerDependencies": {
106 | "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18",
107 | "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18"
108 | }
109 | },
110 | "node_modules/scheduler": {
111 | "version": "0.23.2",
112 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
113 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
114 | "peer": true,
115 | "dependencies": {
116 | "loose-envify": "^1.1.0"
117 | }
118 | },
119 | "node_modules/warning": {
120 | "version": "4.0.3",
121 | "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
122 | "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
123 | "dependencies": {
124 | "loose-envify": "^1.0.0"
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "gsap": "^3.12.5",
4 | "react-modal": "^3.16.1"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/todo:
--------------------------------------------------------------------------------
1 | pending: (v0.2.0)
2 | - fix profile pic uploading error
3 | - enable attachments (image, video, etc.) to notes
4 |
5 | completed:
6 | - adding the loading spinner on every point [DONE]
7 | - complete backend of feedback form [DONE]
8 | - creating different branches for production and development on github [DONE]
9 | - add feedback popup for logged in users [DONE]
10 | - on clicking on a note, new page / modal should appear that display the notes content [DONE]
11 | - name initials bug fixed in profile page [DONE]
12 | - footer enhancement [DONE]
13 | - add a subscriptions section in Hero.jsx page [DONE]
14 | - work on landing page [DONE]
15 | - page responsiveness [DONE]
--------------------------------------------------------------------------------