├── public
├── _redirects
├── robots.txt
├── Udemy.png
├── favicon.ico
├── profile.jpg
├── favicon-32x32.png
├── favicon-64x64.png
├── manifest.json
└── index.html
├── netlify.toml
├── postcss.config.js
├── tailwind.config.js
├── src
├── components
│ ├── Navbar
│ │ ├── Navbar.css
│ │ └── Navbar.jsx
│ └── Footer.jsx
├── index.js
├── pages
│ ├── Notes
│ │ ├── Notes.css
│ │ ├── ViewNoteOfViewNotes.jsx
│ │ ├── ViewNote.jsx
│ │ ├── AddNotes.jsx
│ │ ├── AddNote.jsx
│ │ ├── EditNote.jsx
│ │ ├── ViewCourseNote.jsx
│ │ └── EditNoteOfViewNotes.jsx
│ ├── Courses
│ │ ├── Course.css
│ │ ├── AddCourse.jsx
│ │ └── EditCourse.jsx
│ ├── Home
│ │ ├── Home.css
│ │ └── Home.jsx
│ ├── Projects
│ │ ├── ProjectModal.jsx
│ │ └── Projects.jsx
│ ├── Profile
│ │ └── Profile.jsx
│ ├── Skills
│ │ └── Skills.jsx
│ └── Certificate
│ │ └── Certificate.jsx
├── index.css
├── context
│ └── ThemeContext.js
├── App.js
└── dataService.js
├── .gitignore
├── .github
└── workflows
│ └── deploy.yml
├── LICENSE
├── package.json
└── README.md
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "npm run build"
3 | publish = "build"
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/Udemy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohanmistry231/Udemy-Tracker-Frontend/HEAD/public/Udemy.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohanmistry231/Udemy-Tracker-Frontend/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/profile.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohanmistry231/Udemy-Tracker-Frontend/HEAD/public/profile.jpg
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohanmistry231/Udemy-Tracker-Frontend/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon-64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rohanmistry231/Udemy-Tracker-Frontend/HEAD/public/favicon-64x64.png
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/**/*.{js,jsx,ts,tsx}",
5 | ],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [],
10 | }
--------------------------------------------------------------------------------
/src/components/Navbar/Navbar.css:
--------------------------------------------------------------------------------
1 | @keyframes slideOpacityBloom {
2 | 0% {
3 | transform: translateX(-30%);
4 | opacity: 0;
5 | }
6 | 100% {
7 | transform: translateX(0);
8 | opacity: 1;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .env
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // index.js
2 | import React from "react";
3 | import ReactDOM from "react-dom/client";
4 | import "./index.css";
5 | import App from "./App";
6 | import { ThemeProvider } from "./context/ThemeContext"; // Import ThemeProvider
7 |
8 | const root = ReactDOM.createRoot(document.getElementById("root"));
9 | root.render(
10 |
11 | {/* Wrap the App component with ThemeProvider to provide theme context */}
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/pages/Notes/Notes.css:
--------------------------------------------------------------------------------
1 | /* Hide the button and remove space for larger screens */
2 | @media (min-width: 1024px) {
3 | .hide-on-large {
4 | display: none;
5 | }
6 | }
7 |
8 | /* Show button only for small screens and hide on larger screens */
9 | @media (min-width: 640px) {
10 | /* 640px and above (tablets, desktops, etc.) */
11 | .hide-on-large {
12 | display: none; /* Hide the button and remove it from the layout */
13 | }
14 | }
15 |
16 | @media (max-width: 639px) {
17 | /* 639px and below (phones) */
18 | .hide-on-large {
19 | display: flex; /* Ensure the button is visible */
20 | }
21 | .hide-on-small {
22 | display: none;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/pages/Courses/Course.css:
--------------------------------------------------------------------------------
1 | /* Hide the button and remove space for larger screens */
2 | @media (min-width: 1024px) {
3 | .hide-on-large {
4 | display: none;
5 | }
6 | }
7 |
8 | /* Show button only for small screens and hide on larger screens */
9 | @media (min-width: 640px) {
10 | /* 640px and above (tablets, desktops, etc.) */
11 | .hide-on-large {
12 | display: none; /* Hide the button and remove it from the layout */
13 | }
14 | }
15 |
16 | @media (max-width: 639px) {
17 | /* 639px and below (phones) */
18 | .hide-on-large {
19 | display: flex; /* Ensure the button is visible */
20 | }
21 | .hide-on-small {
22 | display: none;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Own Udemy Tracker",
3 | "description": "A Personal Udemy Courses Tracking Website",
4 | "start_url": ".",
5 | "display": "standalone",
6 | "theme_color": "#000000",
7 | "background_color": "#ffffff",
8 | "icons": [
9 | {
10 | "src": "Udemy.png",
11 | "sizes": "192x192",
12 | "type": "image/png"
13 | },
14 | {
15 | "src": "Udemy.png",
16 | "sizes": "512x512",
17 | "type": "image/png"
18 | },
19 | {
20 | "src": "favicon-64x64.png",
21 | "sizes": "64x64",
22 | "type": "image/png"
23 | },
24 | {
25 | "src": "favicon-32x32.png",
26 | "sizes": "32x32",
27 | "type": "image/png"
28 | }
29 | ]
30 | }
--------------------------------------------------------------------------------
/src/pages/Home/Home.css:
--------------------------------------------------------------------------------
1 | @keyframes slide-small {
2 | from {
3 | transform: translateX(100%);
4 | }
5 | to {
6 | transform: translateX(-100%);
7 | }
8 | }
9 |
10 | @keyframes slide-large {
11 | from {
12 | transform: translateX(420%); /* Start from halfway */
13 | }
14 | to {
15 | transform: translateX(-100%); /* End at halfway */
16 | }
17 | }
18 |
19 | .animate-slide-text {
20 | animation-timing-function: linear;
21 | animation-iteration-count: infinite;
22 | }
23 |
24 | @media (max-width: 1023px) {
25 | /* Small and medium screens */
26 | .animate-slide-text {
27 | animation-name: slide-small;
28 | }
29 | }
30 |
31 | @media (min-width: 1024px) {
32 | /* Larger screens */
33 | .animate-slide-text {
34 | animation-name: slide-large;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to Netlify
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build-and-deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 |
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v3
18 | with:
19 | node-version: '18'
20 |
21 | - name: Install dependencies
22 | run: npm install
23 |
24 | - name: Build project
25 | run: npm run build
26 |
27 | - name: Deploy to Netlify
28 | uses: netlify/actions/cli@master
29 | with:
30 | args: deploy --dir=build --prod
31 | env:
32 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
33 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 rohanmistry231
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.
--------------------------------------------------------------------------------
/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | // src/components/Footer.js
2 | import React from "react";
3 | import { useTheme } from "../context/ThemeContext"; // Adjust the path if necessary
4 |
5 | const Footer = () => {
6 | const { theme } = useTheme();
7 | const isDarkMode = theme === "dark"; // Check if dark mode is active
8 |
9 | return (
10 |
33 | );
34 | };
35 |
36 | export default Footer;
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "udemy-tracker",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.17.0",
7 | "@testing-library/react": "^13.4.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "@tinymce/tinymce-react": "^5.1.1",
10 | "axios": "^1.7.7",
11 | "chart.js": "^4.4.5",
12 | "jspdf": "^2.5.2",
13 | "react": "^18.3.1",
14 | "react-chartjs-2": "^5.2.0",
15 | "react-dom": "^18.3.1",
16 | "react-icons": "^5.3.0",
17 | "react-router-dom": "^6.27.0",
18 | "react-scripts": "5.0.1",
19 | "react-select": "^5.8.2",
20 | "react-spring": "^9.7.4",
21 | "react-toastify": "^10.0.6",
22 | "tailwindcss": "^3.4.14",
23 | "web-vitals": "^2.1.4"
24 | },
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject"
30 | },
31 | "eslintConfig": {
32 | "extends": [
33 | "react-app",
34 | "react-app/jest"
35 | ]
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | },
49 | "devDependencies": {
50 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | margin: 0;
7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
9 | sans-serif;
10 | -webkit-font-smoothing: antialiased;
11 | -moz-osx-font-smoothing: grayscale;
12 | transition: background-color 0.3s ease; /* Smooth transition for background color */
13 | }
14 |
15 | code {
16 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
17 | monospace;
18 | }
19 |
20 | /* General scrollbar styling for WebKit browsers (Chrome, Safari, etc.) */
21 | ::-webkit-scrollbar {
22 | width: 6px; /* Thin scrollbar track */
23 | height: 6px; /* For horizontal scrollbar */
24 | }
25 |
26 | ::-webkit-scrollbar-track {
27 | background-color: var(--scrollbar-track-color);
28 | }
29 |
30 | ::-webkit-scrollbar-thumb {
31 | background-color: var(--scrollbar-thumb-color);
32 | border-radius: 10px;
33 | width: 12px; /* Larger thumb for easier visibility */
34 | }
35 |
36 | ::-webkit-scrollbar-thumb:hover {
37 | background-color: var(--scrollbar-thumb-hover-color);
38 | }
39 |
40 | /* Firefox styling */
41 | * {
42 | scrollbar-width: thin; /* Makes the scrollbar track thin */
43 | scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); /* Thumb color and track color */
44 | }
45 |
46 | /* Hover effect for Firefox, using the pseudo-class */
47 | *:hover {
48 | scrollbar-color: var(--scrollbar-thumb-hover-color) var(--scrollbar-track-color);
49 | }
50 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | Own Udemy Tracker
27 |
28 |
29 | You need to enable JavaScript to run this app.
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/context/ThemeContext.js:
--------------------------------------------------------------------------------
1 | import { createContext, useState, useContext, useEffect } from "react";
2 |
3 | // Updated Colors based on Udemy palette
4 | const themes = {
5 | dark: {
6 | background: "#111827",
7 | color: "#e5e7eb",
8 | scrollbarTrack: "#1f2937", // Darker gray for track
9 | scrollbarThumb: "#4b5563", // Medium gray for thumb
10 | scrollbarThumbHover: "#6b7280", // Slightly lighter gray for hover
11 | },
12 | light: {
13 | background: "#ffffff",
14 | color: "#111827",
15 | scrollbarTrack: "#e5e7eb", // Light gray for track
16 | scrollbarThumb: "#111827", // Dark color for thumb
17 | scrollbarThumbHover: "#374151", // Slightly darker gray for hover
18 | },
19 | };
20 |
21 | const ThemeContext = createContext();
22 |
23 | export const ThemeProvider = ({ children }) => {
24 | const [theme, setTheme] = useState(localStorage.getItem("theme") || "light");
25 |
26 | const toggleTheme = () => {
27 | const newTheme = theme === "light" ? "dark" : "light";
28 | setTheme(newTheme);
29 | localStorage.setItem("theme", newTheme);
30 | };
31 |
32 | useEffect(() => {
33 | const currentTheme = themes[theme];
34 | document.body.style.backgroundColor = currentTheme.background;
35 | document.body.style.color = currentTheme.color;
36 |
37 | // Apply scrollbar colors based on the theme
38 | document.documentElement.style.setProperty(
39 | "--scrollbar-track-color",
40 | currentTheme.scrollbarTrack
41 | );
42 | document.documentElement.style.setProperty(
43 | "--scrollbar-thumb-color",
44 | currentTheme.scrollbarThumb
45 | );
46 | document.documentElement.style.setProperty(
47 | "--scrollbar-thumb-hover-color",
48 | currentTheme.scrollbarThumbHover
49 | );
50 | }, [theme]);
51 |
52 | return (
53 |
54 | {children}
55 |
56 | );
57 | };
58 |
59 | export const useTheme = () => useContext(ThemeContext);
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | // src/App.js
2 | import React, { useState } from "react";
3 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
4 | import Navbar from "./components/Navbar/Navbar";
5 | import Footer from "./components/Footer";
6 | import Courses from "./pages/Courses/Courses";
7 | import AddCourse from "./pages/Courses/AddCourse";
8 | import EditCourse from "./pages/Courses/EditCourse";
9 | import ViewCourse from "./pages/Courses/ViewCourse";
10 | import AddNotes from "./pages/Notes/AddNotes";
11 | import ViewNotes from "./pages/Notes/ViewNotes";
12 | import AddNote from "./pages/Notes/AddNote"; // Import the new AddNote page
13 | import { ThemeProvider, useTheme } from "./context/ThemeContext";
14 | import Home from "./pages/Home/Home";
15 | import Profile from "./pages/Profile/Profile";
16 | import Progress from "./pages/Progress/Progress";
17 | import Notes from "./pages/Notes/Notes";
18 | import EditNote from "./pages/Notes/EditNote";
19 | import ViewNote from "./pages/Notes/ViewNote";
20 | import ViewCourseNote from "./pages/Notes/ViewCourseNote";
21 | import ViewNoteOfViewNotes from "./pages/Notes/ViewNoteOfViewNotes";
22 | import EditNoteOfViewNotes from "./pages/Notes/EditNoteOfViewNotes";
23 | import Certificate from "./pages/Certificate/Certificate";
24 | import Skills from "./pages/Skills/Skills";
25 | import Projects from "./pages/Projects/Projects";
26 |
27 | function App() {
28 | const { isDarkMode } = useTheme();
29 | const [courses, setCourses] = useState([]);
30 |
31 | const handleAddCourse = (newCourse) => {
32 | setCourses((prevCourses) => [...prevCourses, newCourse]);
33 | };
34 |
35 | return (
36 |
37 |
42 |
43 |
44 |
45 | } />
46 | } />
47 | }
50 | />
51 | } />
52 | } />
53 | } />
54 | } />
55 | }
58 | />
59 | } />
60 | } />
61 | }
64 | />
65 | }
68 | />
69 | } />
70 | } />
71 | } />
72 | } />
73 | } />
74 | } />
75 | } />
76 | {/* New route for AddNote */}
77 |
78 |
79 |
80 |
81 |
82 | );
83 | }
84 |
85 | const MainApp = () => (
86 |
87 |
88 |
89 | );
90 |
91 | export default MainApp;
92 |
--------------------------------------------------------------------------------
/src/pages/Notes/ViewNoteOfViewNotes.jsx:
--------------------------------------------------------------------------------
1 | // src/pages/ViewNote.js
2 | import React, { useState, useEffect } from "react";
3 | import { useParams, Link } from "react-router-dom";
4 | import { ToastContainer, toast } from "react-toastify";
5 | import "react-toastify/dist/ReactToastify.css";
6 | import { useTheme } from "../../context/ThemeContext";
7 | import jsPDF from "jspdf"; // Import jsPDF
8 | import { fetchNoteById } from "../../dataService";
9 |
10 | const ViewNoteOfViewNotes = () => {
11 | const correctPassword = "12345";
12 | const { courseid, id } = useParams(); // Note ID for fetching specific note details
13 | const { theme } = useTheme();
14 | const isDarkMode = theme === "dark";
15 | const [note, setNote] = useState(null);
16 | const [password, setPassword] = useState("");
17 | const [isAuthorized, setIsAuthorized] = useState(false);
18 |
19 | useEffect(() => {
20 | const fetchNoteDetails = async () => {
21 | try {
22 | const data = await fetchNoteById(id); // Call the service function to fetch the note
23 | setNote(data.note); // Update state with the fetched note
24 | const storedPassword = localStorage.getItem("password");
25 | if (storedPassword === correctPassword) {
26 | setIsAuthorized(true);
27 | }
28 | } catch (error) {
29 | console.error("Error fetching note:", error);
30 | toast.error("Error fetching note details");
31 | }
32 | };
33 |
34 | if (id) {
35 | fetchNoteDetails(); // Fetch the note details when the component mounts or id changes
36 | }
37 | }, [id]);
38 |
39 | const saveAsPDF = () => {
40 | // Function to strip HTML tags
41 | const stripHtml = (html) => {
42 | const tempDiv = document.createElement("div");
43 | tempDiv.innerHTML = html;
44 | return tempDiv.textContent || tempDiv.innerText || "";
45 | };
46 |
47 | // Decode the answer content
48 | const cleanAnswer = stripHtml(note.answer);
49 | const pdf = new jsPDF();
50 |
51 | pdf.setFontSize(20);
52 | pdf.text("Note Details", 10, 10);
53 |
54 | pdf.setFontSize(12);
55 | pdf.text(`Question: ${note.question}`, 10, 20);
56 | pdf.text(`Answer: ${cleanAnswer}`, 10, 30);
57 | pdf.text(`Main Target Category: ${note.mainTargetCategory}`, 10, 40);
58 | pdf.text(`Main Target Goal: ${note.mainTargetGoal}`, 10, 50);
59 | pdf.text(`Sub Target Goal: ${note.subTargetGoal}`, 10, 60);
60 |
61 | pdf.save(`note_${id}.pdf`);
62 | };
63 |
64 | const handlePasswordSubmit = (e) => {
65 | e.preventDefault();
66 | const correctPassword = "12345";
67 | if (password === correctPassword) {
68 | setIsAuthorized(true);
69 | localStorage.setItem("password", password); // Store the password in localStorage
70 | toast.success("Access granted!");
71 | } else {
72 | toast.error("Incorrect password. Please try again.");
73 | }
74 | };
75 |
76 | return (
77 |
82 | {!isAuthorized ? (
83 |
109 | ) : (
110 | <>
111 |
116 |
Note Details
117 |
118 |
Question:
119 |
{note.question}
120 |
121 |
122 |
Answer:
123 |
124 |
125 |
126 |
Main Target Category:
127 |
{note.mainTargetCategory}
128 |
129 |
130 |
Main Target Goal:
131 |
{note.mainTargetGoal}
132 |
133 |
134 |
Sub Target Goal:
135 |
{note.subTargetGoal}
136 |
137 |
138 |
139 |
143 | Edit Note
144 |
145 |
149 | Back to Course Notes
150 |
151 |
155 | Save as PDF
156 |
157 |
158 |
159 | >
160 | )}
161 |
162 |
163 | );
164 | };
165 |
166 | export default ViewNoteOfViewNotes;
167 |
--------------------------------------------------------------------------------
/src/pages/Notes/ViewNote.jsx:
--------------------------------------------------------------------------------
1 | // src/pages/ViewNote.js
2 | import React, { useState, useEffect } from "react";
3 | import { useParams, Link } from "react-router-dom";
4 | import { ToastContainer, toast } from "react-toastify";
5 | import "react-toastify/dist/ReactToastify.css";
6 | import { useTheme } from "../../context/ThemeContext";
7 | import jsPDF from "jspdf"; // Import jsPDF
8 | import { fetchNoteById } from "../../dataService";
9 | import html2canvas from "html2canvas";
10 |
11 | const ViewNote = () => {
12 | const correctPassword = "12345";
13 | const { id } = useParams(); // Note ID for fetching specific note details
14 | const { theme } = useTheme();
15 | const isDarkMode = theme === "dark";
16 | const [note, setNote] = useState(null);
17 | const [password, setPassword] = useState("");
18 | const [isAuthorized, setIsAuthorized] = useState(false);
19 |
20 | useEffect(() => {
21 | const fetchNoteDetails = async () => {
22 | try {
23 | const data = await fetchNoteById(id); // Call the service function
24 | setNote(data.note); // Set the note details from the response
25 | const storedPassword = localStorage.getItem("password");
26 | if (storedPassword === correctPassword) {
27 | setIsAuthorized(true);
28 | }
29 | } catch (error) {
30 | console.error("Error fetching note:", error);
31 | toast.error("Error fetching note details");
32 | }
33 | };
34 |
35 | fetchNoteDetails(); // Fetch the note details when the component mounts or id changes
36 | }, [id]);
37 |
38 | const saveAsPDF = () => {
39 | // Create a container for the HTML content we want to capture
40 | const container = document.createElement("div");
41 | container.style.position = "absolute";
42 | container.style.top = "-9999px";
43 | container.style.fontFamily = "Arial, sans-serif";
44 | container.style.lineHeight = "1.6";
45 | container.innerHTML = `
46 |
47 |
Note Details
48 |
Question: ${note.question}
49 |
Main Goal: ${note.mainTargetCategory}
50 |
Target Goal: ${note.mainTargetGoal}
51 |
Sub Goal: ${note.subTargetGoal}
52 |
Answer:
53 |
${note.answer}
54 |
55 | `;
56 | document.body.appendChild(container);
57 |
58 | // Render the content with html2canvas at a higher scale for better clarity
59 | html2canvas(container, { scale: 3 }).then((canvas) => {
60 | const imgData = canvas.toDataURL("image/png");
61 | const pdf = new jsPDF("p", "mm", "a4");
62 | const pdfWidth = pdf.internal.pageSize.getWidth();
63 | const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
64 |
65 | // Adjust the image size and margins for better output
66 | pdf.addImage(imgData, "PNG", 10, 10, pdfWidth - 20, pdfHeight - 10);
67 | pdf.save(`note_${note._id}.pdf`);
68 |
69 | // Clean up by removing the temporary container
70 | document.body.removeChild(container);
71 | });
72 | };
73 |
74 | const handlePasswordSubmit = (e) => {
75 | e.preventDefault();
76 | const correctPassword = "12345";
77 | if (password === correctPassword) {
78 | setIsAuthorized(true);
79 | localStorage.setItem("password", password); // Store the password in localStorage
80 | toast.success("Access granted!");
81 | } else {
82 | toast.error("Incorrect password. Please try again.");
83 | }
84 | };
85 |
86 | return (
87 |
92 | {!isAuthorized ? (
93 |
119 | ) : (
120 | <>
121 |
126 |
Note Details
127 |
128 |
Question:
129 |
{note.question}
130 |
131 |
132 |
Answer:
133 |
134 |
135 |
136 |
Main Target Category:
137 |
{note.mainTargetCategory}
138 |
139 |
140 |
Main Target Goal:
141 |
{note.mainTargetGoal}
142 |
143 |
144 |
Sub Target Goal:
145 |
{note.subTargetGoal}
146 |
147 |
148 |
149 |
153 | Edit Note
154 |
155 |
156 | Back to Notes
157 |
158 |
162 | Save as PDF
163 |
164 |
165 |
166 | >
167 | )}
168 |
169 |
170 | );
171 | };
172 |
173 | export default ViewNote;
174 |
--------------------------------------------------------------------------------
/src/components/Navbar/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Link, useLocation } from "react-router-dom";
3 | import { useTheme } from "../../context/ThemeContext";
4 | import "./Navbar.css";
5 |
6 | const Navbar = () => {
7 | const { theme, toggleTheme } = useTheme();
8 | const [isOpen, setIsOpen] = useState(false);
9 | const location = useLocation(); // Use useLocation hook to track current path
10 |
11 | const toggleMenu = () => {
12 | setIsOpen(!isOpen);
13 | };
14 |
15 | const isDarkMode = theme === "dark";
16 |
17 | const getLinkClass = (path) => {
18 | const isActive = location.pathname === path;
19 | return isActive
20 | ? "text-purple-500" // Active link in purple
21 | : `${
22 | isDarkMode ? "text-gray-300" : "text-gray-800"
23 | } hover:text-purple-500`; // Default link style
24 | };
25 |
26 | return (
27 |
32 |
33 |
34 |
35 |
41 | Udemy Tracker
42 |
43 |
44 |
45 |
46 |
50 | Home
51 |
52 |
56 | Courses
57 |
58 |
62 | Notes
63 |
64 |
68 | Skills
69 |
70 |
74 | Projects
75 |
76 |
80 | Progress
81 |
82 |
88 | Certificates
89 |
90 |
94 | Profile
95 |
96 |
105 | {isDarkMode ? "☀️ Light" : "🌙 Dark"}
106 |
107 |
108 |
109 |
110 |
119 | {isDarkMode ? "☀️" : "🌙"}
120 |
121 |
127 | {isOpen ? (
128 |
135 |
141 |
142 | ) : (
143 |
150 |
156 |
157 | )}
158 |
159 |
160 |
161 |
162 |
163 |
170 | {[
171 | "/",
172 | "/courses",
173 | "/notes",
174 | "/skills",
175 | "/projects",
176 | "/progress",
177 | "/certificate",
178 | "/profile",
179 | ].map((path, index) => (
180 |
195 | {path === "/"
196 | ? "Home"
197 | : path.replace("/", "").charAt(0).toUpperCase() + path.slice(2)}
198 |
199 | ))}
200 |
201 |
202 | );
203 | };
204 |
205 | export default Navbar;
206 |
--------------------------------------------------------------------------------
/src/pages/Projects/ProjectModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useTheme } from "../../context/ThemeContext"; // Importing the useTheme hook
3 |
4 | // Dummy data for categories and subcategories
5 | const projectCategories = [
6 | {
7 | name: "Data Science",
8 | subcategories: [
9 | "Machine Learning",
10 | "Artificial Intelligence",
11 | "Deep Learning",
12 | ],
13 | },
14 | {
15 | name: "Web Development",
16 | subcategories: ["Frontend", "Backend", "Full Stack"],
17 | },
18 | {
19 | name: "Mobile Development",
20 | subcategories: ["Android", "iOS", "React Native"],
21 | },
22 | {
23 | name: "DevOps",
24 | subcategories: ["CI/CD", "Cloud Computing", "Infrastructure Automation"],
25 | },
26 | ];
27 |
28 | const ProjectModal = ({ project = {}, onClose, onSubmit }) => {
29 | const { theme } = useTheme(); // Access the theme
30 | const isDarkMode = theme === "dark"; // Check if dark mode is enabled
31 |
32 | const [formData, setFormData] = useState({
33 | title: project.title || "",
34 | description: project.description || "",
35 | tech: project.tech ? project.tech.join(", ") : "",
36 | link: project.link || "",
37 | liveDemo: project.liveDemo || "",
38 | category: project.category || "", // New category field
39 | subCategory: project.subCategory || "", // New sub-category field
40 | });
41 |
42 | const [subCategories, setSubCategories] = useState([]);
43 |
44 | useEffect(() => {
45 | // Set subcategories based on selected category
46 | const categoryData = projectCategories.find(
47 | (cat) => cat.name === formData.category
48 | );
49 | if (categoryData) {
50 | setSubCategories(categoryData.subcategories);
51 | } else {
52 | setSubCategories([]);
53 | }
54 | }, [formData.category]);
55 |
56 | const handleInputChange = (e) => {
57 | const { name, value } = e.target;
58 | setFormData((prev) => ({
59 | ...prev,
60 | [name]: value,
61 | }));
62 | };
63 |
64 | const handleFormSubmit = (e) => {
65 | e.preventDefault();
66 | // Convert tech string into an array
67 | const techArray = formData.tech.split(",").map((item) => item.trim());
68 | onSubmit({ ...project, ...formData, tech: techArray });
69 | };
70 |
71 | return (
72 |
77 |
82 |
83 | {project._id ? "Update Project" : "Add Project"}
84 |
85 |
86 |
261 |
262 |
263 | );
264 | };
265 |
266 | export default ProjectModal;
267 |
--------------------------------------------------------------------------------
/src/pages/Profile/Profile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useTheme } from "../../context/ThemeContext";
3 | import { Link } from "react-router-dom";
4 | import { getCoursesFromLocalStorage } from "../../dataService";
5 | import { FaGlobe, FaLinkedin, FaGithub, FaMedium } from "react-icons/fa";
6 |
7 | const Profile = () => {
8 | const { theme } = useTheme();
9 | const [courses, setCourses] = useState([]);
10 | const [completedCoursesCount, setCompletedCoursesCount] = useState(0);
11 | const isDarkMode = theme === "dark";
12 | const [isLoading, setIsLoading] = useState(true);
13 |
14 | const [user, setUser] = useState({
15 | name: "",
16 | email: "",
17 | bio: "",
18 | avatarUrl: "", // Placeholder for user avatar
19 | socialLinks: {
20 | portfolio: "", // Default empty string to avoid undefined errors
21 | linkedin: "",
22 | github: "",
23 | medium: "",
24 | },
25 | });
26 |
27 | useEffect(() => {
28 | setIsLoading(true); // Set loading to true when fetching starts
29 |
30 | // Function to get courses from localStorage
31 | const fetchCoursesFromLocalStorage = () => {
32 | try {
33 | const storedCourses = getCoursesFromLocalStorage(); // Get courses from localStorage
34 |
35 | if (storedCourses.length > 0) {
36 | // If courses are found in localStorage, update state
37 | setCourses(storedCourses);
38 |
39 | // Count completed courses
40 | const completedCount = storedCourses.filter(
41 | (course) => course.status === "Completed"
42 | ).length;
43 | setCompletedCoursesCount(completedCount);
44 | } else {
45 | console.error("No courses found in localStorage.");
46 | }
47 | } catch (error) {
48 | console.error("Error fetching courses from localStorage:", error);
49 | } finally {
50 | setIsLoading(false); // Set loading to false when fetching completes
51 | }
52 | };
53 |
54 | fetchCoursesFromLocalStorage(); // Call function to fetch data from localStorage
55 | }, []);
56 |
57 | // Key for localStorage
58 | const localStorageKey = "userProfile";
59 |
60 | // Save data to localStorage
61 | const saveToLocalStorage = (data) => {
62 | localStorage.setItem(localStorageKey, JSON.stringify(data));
63 | };
64 |
65 | // Load data from localStorage
66 | const loadFromLocalStorage = () => {
67 | const storedData = localStorage.getItem(localStorageKey);
68 | return storedData ? JSON.parse(storedData) : null;
69 | };
70 |
71 | // Simulate fetching user data from API
72 | useEffect(() => {
73 | const fetchUserData = async () => {
74 | try {
75 | setIsLoading(true);
76 |
77 | // Check localStorage for user data
78 | const storedData = loadFromLocalStorage();
79 | if (storedData) {
80 | setUser(storedData);
81 | } else {
82 | // Simulate fetching user data (replace with actual API call)
83 | const userData = {
84 | name: "Rohan Mistry",
85 | email: "rohanmistry231@gmail.com",
86 | bio: "A passionate learner exploring the world of technology.",
87 | avatarUrl: "profile.jpg", // Example avatar URL
88 | socialLinks: {
89 | portfolio: "https://irohanportfolio.netlify.app",
90 | linkedin: "https://linkedin.com/in/rohan-mistry-493987202",
91 | github: "https://github.com/rohanmistry231",
92 | medium: "https://medium.com/@rohanmistry231",
93 | },
94 | };
95 |
96 | // Set user data and save to localStorage
97 | setUser(userData);
98 | saveToLocalStorage(userData);
99 | }
100 | } catch (error) {
101 | console.error("Error fetching user data:", error);
102 | } finally {
103 | setIsLoading(false);
104 | }
105 | };
106 |
107 | fetchUserData();
108 | }, []);
109 |
110 | return (
111 |
116 | {/* Profile Card */}
117 |
122 | {isLoading ? (
123 |
126 | ) : (
127 | <>
128 | {/* Profile Header */}
129 |
130 |
136 |
145 |
146 |
147 | {/* Bio Section */}
148 |
149 |
Bio
150 |
155 | {user.bio}
156 |
157 |
158 |
159 | {/* Additional Information Section */}
160 |
161 |
162 |
Progress Overview
163 |
164 |
165 | View Full Progress
166 |
167 |
168 |
169 |
170 |
171 | {/* Total Courses Card */}
172 |
177 |
Total Courses
178 |
179 | {courses.length}
180 |
181 |
182 |
183 | {/* Completed Courses Card */}
184 |
189 |
Completed Courses
190 |
191 | {completedCoursesCount}
192 |
193 |
194 |
195 |
196 |
197 | {/* Social Media Links */}
198 |
199 |
Social Media Links
200 |
238 |
239 | >
240 | )}
241 |
242 |
243 | );
244 | };
245 |
246 | export default Profile;
247 |
--------------------------------------------------------------------------------
/src/pages/Notes/AddNotes.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams, useNavigate } from "react-router-dom";
3 | import { toast } from "react-toastify";
4 | import "react-toastify/dist/ReactToastify.css";
5 | import { useTheme } from "../../context/ThemeContext"; // Import theme context
6 | import { addNoteToCourse, getCourseDetails } from "../../dataService";
7 | import { categories, targetGoals, subGoals } from "../../db";
8 | import { Editor } from "@tinymce/tinymce-react";
9 |
10 | const AddNotes = () => {
11 | const handleEditorChange = (content) => setAnswer(content);
12 | const correctPassword = "12345";
13 | const { id } = useParams();
14 | const navigate = useNavigate();
15 | const { theme } = useTheme(); // Use theme context
16 | const isDarkMode = theme === "dark"; // Check if dark mode is enabled
17 |
18 | const [question, setQuestion] = useState("");
19 | const [answer, setAnswer] = useState("");
20 | const [courseName, setCourseName] = useState("");
21 | const [password, setPassword] = useState("");
22 | const [isAuthorized, setIsAuthorized] = useState(false);
23 |
24 | const [mainCategory, setMainCategory] = useState("");
25 | const [targetGoal, setTargetGoal] = useState("");
26 | const [subTargetGoal, setSubTargetGoal] = useState("");
27 |
28 | useEffect(() => {
29 | const fetchCourseData = async () => {
30 | try {
31 | // Fetch course name and category details
32 | const courseDetails = getCourseDetails(id);
33 | setCourseName(courseDetails.name); // Assuming courseDetails contains name
34 | setMainCategory(courseDetails.mainCategory);
35 | setTargetGoal(courseDetails.targetGoal);
36 |
37 | // Check if user is already authorized
38 | const storedPassword = localStorage.getItem("password");
39 | if (storedPassword === correctPassword) {
40 | setIsAuthorized(true);
41 | }
42 | } catch (error) {
43 | console.error("Error fetching course data:", error);
44 | setCourseName("Error fetching course name"); // Optional fallback
45 | }
46 | };
47 |
48 | fetchCourseData();
49 | }, [id]);
50 |
51 | const handleAddNote = async (e) => {
52 | e.preventDefault();
53 |
54 | const noteData = {
55 | question,
56 | answer,
57 | mainTargetCategory: mainCategory,
58 | mainTargetGoal: targetGoal,
59 | subTargetGoal,
60 | };
61 |
62 | try {
63 | // Call the service function to add the note
64 | await addNoteToCourse(id, noteData);
65 |
66 | // Navigate to the notes page after successful addition
67 | navigate(`/courses/${id}/notes`);
68 | toast.success("Note added successfully!");
69 | } catch (error) {
70 | console.error("Error adding note:", error);
71 | toast.error("Failed to add note. Please try again.");
72 | }
73 | };
74 |
75 | const handlePasswordSubmit = (e) => {
76 | e.preventDefault();
77 | const correctPassword = "12345";
78 | if (password === correctPassword) {
79 | setIsAuthorized(true);
80 | localStorage.setItem("password", password); // Store the password in localStorage
81 | toast.success("Access granted!");
82 | } else {
83 | toast.error("Incorrect password. Please try again.");
84 | }
85 | };
86 |
87 | return (
88 |
93 | {!isAuthorized ? (
94 |
100 |
101 | 🔒 Prove You're Worthy! Enter the Secret Code:
102 |
103 | setPassword(e.target.value)}
108 | className={`border p-2 rounded w-full ${
109 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
110 | }`}
111 | required
112 | />
113 |
117 | Submit
118 |
119 |
120 | ) : (
121 | <>
122 |
127 |
128 | Add Notes for {courseName}
129 |
130 | {console.log(mainCategory)}
131 | {console.log(targetGoal)}
132 |
133 | {/* Main Category Select */}
134 | setMainCategory(mainCategory)}
137 | className={`border p-2 rounded w-full ${
138 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
139 | }`}
140 | disabled
141 | >
142 | Select Main Category
143 | {categories.map((category) => (
144 |
145 | {category}
146 |
147 | ))}
148 |
149 |
150 | {/* Target Goal Select */}
151 | setTargetGoal(e.target.value)}
154 | className={`border p-2 rounded w-full ${
155 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
156 | }`}
157 | disabled
158 | >
159 | Select Target Goal
160 | {mainCategory &&
161 | targetGoals[mainCategory]?.map((goal) => (
162 |
163 | {goal}
164 |
165 | ))}
166 |
167 |
168 | {/* Sub Target Goal Select */}
169 | setSubTargetGoal(e.target.value)}
172 | className={`border p-2 rounded w-full ${
173 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
174 | }`}
175 | disabled={!targetGoal}
176 | >
177 | Select Sub Target Goal
178 | {targetGoal &&
179 | subGoals[targetGoal]?.map((subGoal) => (
180 |
181 | {subGoal}
182 |
183 | ))}
184 |
185 |
186 | {/* Question Input */}
187 |
188 |
189 | Question:
190 |
191 | setQuestion(e.target.value)}
197 | className={`border p-2 rounded w-full ${
198 | isDarkMode
199 | ? "bg-gray-700 text-white"
200 | : "bg-white text-black"
201 | }`}
202 | required
203 | />
204 |
205 |
206 | {/* Answer Textarea */}
207 |
208 |
209 | Answer:
210 |
211 |
238 |
239 |
240 |
244 | Add Note
245 |
246 |
247 |
248 | >
249 | )}
250 |
251 | );
252 | };
253 |
254 | export default AddNotes;
255 |
--------------------------------------------------------------------------------
/src/pages/Notes/AddNote.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { toast } from "react-toastify";
4 | import "react-toastify/dist/ReactToastify.css";
5 | import { useTheme } from "../../context/ThemeContext";
6 | import Select from "react-select";
7 | import {
8 | getCoursesFromLocalStorage,
9 | getNotesFromLocalStorage,
10 | saveNotesToLocalStorage,
11 | } from "../../dataService";
12 | import { categories, targetGoals, subGoals } from "../../db";
13 | import { Editor } from "@tinymce/tinymce-react";
14 |
15 | const AddNote = () => {
16 | const handleEditorChange = (content) => setAnswer(content);
17 | const correctPassword = "12345";
18 | const navigate = useNavigate();
19 | const { theme } = useTheme();
20 | const isDarkMode = theme === "dark";
21 |
22 | const [courses, setCourses] = useState([]);
23 | const [selectedCourse, setSelectedCourse] = useState(null);
24 | const [question, setQuestion] = useState("");
25 | const [answer, setAnswer] = useState("");
26 | const [mainCategory, setMainCategory] = useState("");
27 | const [targetGoal, setTargetGoal] = useState("");
28 | const [subTargetGoal, setSubTargetGoal] = useState("");
29 | const [password, setPassword] = useState("");
30 | const [isAuthorized, setIsAuthorized] = useState(false);
31 |
32 | useEffect(() => {
33 | const fetchCourses = () => {
34 | try {
35 | const storedCourses = getCoursesFromLocalStorage();
36 | const formattedCourses = storedCourses.map((course) => ({
37 | value: course._id,
38 | label: course.name,
39 | mainCategory: course.category,
40 | targetGoal: course.subCategory,
41 | }));
42 | setCourses(formattedCourses);
43 |
44 | const storedPassword = localStorage.getItem("password");
45 | if (storedPassword === correctPassword) {
46 | setIsAuthorized(true);
47 | }
48 | } catch (error) {
49 | console.error("Error fetching courses from localStorage:", error);
50 | }
51 | };
52 |
53 | fetchCourses();
54 | }, []);
55 |
56 | const handleCourseChange = (selectedOption) => {
57 | setSelectedCourse(selectedOption);
58 | if (selectedOption) {
59 | setMainCategory(selectedOption.mainCategory || "");
60 | setTargetGoal(selectedOption.targetGoal || "");
61 | setSubTargetGoal("");
62 | } else {
63 | setMainCategory("");
64 | setTargetGoal("");
65 | setSubTargetGoal("");
66 | }
67 | };
68 |
69 | const handleAddNote = async (e) => {
70 | e.preventDefault();
71 | if (!selectedCourse) {
72 | toast.error("Please select a course.");
73 | return;
74 | }
75 |
76 | try {
77 | const newNote = {
78 | question,
79 | answer,
80 | mainTargetCategory: mainCategory,
81 | mainTargetGoal: targetGoal,
82 | subTargetGoal,
83 | };
84 |
85 | const response = await fetch(
86 | `https://udemy-tracker.vercel.app/courses/${selectedCourse.value}/notes`,
87 | {
88 | method: "POST",
89 | headers: { "Content-Type": "application/json" },
90 | body: JSON.stringify(newNote),
91 | }
92 | );
93 |
94 | if (!response.ok) {
95 | throw new Error("Failed to add note");
96 | }
97 |
98 | const newNoteData = await response.json();
99 | let storedNotes = getNotesFromLocalStorage() || {};
100 | if (!storedNotes[selectedCourse.value]) {
101 | storedNotes[selectedCourse.value] = [];
102 | }
103 | storedNotes[selectedCourse.value].push(newNoteData);
104 | saveNotesToLocalStorage(storedNotes);
105 |
106 | toast.success("Note added successfully!");
107 | navigate(`/courses/${selectedCourse.value}/view`);
108 | } catch (error) {
109 | console.error("Error adding note:", error);
110 | toast.error("Failed to add note. Please try again.");
111 | }
112 | };
113 |
114 | const handlePasswordSubmit = (e) => {
115 | e.preventDefault();
116 | if (password === correctPassword) {
117 | setIsAuthorized(true);
118 | localStorage.setItem("password", password);
119 | toast.success("Access granted!");
120 | } else {
121 | toast.error("Incorrect password. Please try again.");
122 | }
123 | };
124 |
125 | return (
126 |
286 | );
287 | };
288 |
289 | export default AddNote;
290 |
--------------------------------------------------------------------------------
/src/pages/Skills/Skills.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useTheme } from "../../context/ThemeContext";
3 | import * as FaIcons from "react-icons/fa";
4 | import * as SiIcons from "react-icons/si";
5 | import { FiEdit } from "react-icons/fi";
6 | import { AiOutlineDelete, AiOutlinePlus } from "react-icons/ai";
7 | import axios from "axios";
8 |
9 | const Skills = () => {
10 | const { theme } = useTheme();
11 | const isDarkMode = theme === "dark";
12 |
13 | const correctPassword = "12345";
14 | const [loading, setLoading] = useState(true); // Loading state
15 | const [skills, setSkills] = useState([]);
16 | const [showModal, setShowModal] = useState(false);
17 | const [currentSkill, setCurrentSkill] = useState(null); // For editing
18 | const [formData, setFormData] = useState({
19 | name: "",
20 | description: "",
21 | level: "",
22 | icon: "",
23 | });
24 |
25 | useEffect(() => {
26 | const fetchSkills = async () => {
27 | try {
28 | const response = await axios.get(
29 | "https://udemy-tracker.vercel.app/skill"
30 | );
31 | setSkills(response.data);
32 | } catch (err) {
33 | console.error("Error fetching skills data");
34 | } finally {
35 | setLoading(false);
36 | }
37 | };
38 |
39 | fetchSkills();
40 | }, []);
41 |
42 | // Dynamically render the icon component based on the icon name
43 | const renderIcon = (iconName) => {
44 | let IconComponent = FaIcons[iconName] || SiIcons[iconName];
45 | return IconComponent ? (
46 |
47 | ) : (
48 | (No Icon)
49 | );
50 | };
51 |
52 | // Handle form input changes
53 | const handleChange = (e) => {
54 | setFormData({ ...formData, [e.target.name]: e.target.value });
55 | };
56 |
57 | // Handle adding/updating a skill
58 | const handleSave = async () => {
59 | try {
60 | if (currentSkill) {
61 | // Update skill
62 | await axios.put(
63 | `https://udemy-tracker.vercel.app/skill/${currentSkill._id}`,
64 | formData
65 | );
66 | setSkills((prevSkills) =>
67 | prevSkills.map((skill) =>
68 | skill._id === currentSkill._id ? { ...skill, ...formData } : skill
69 | )
70 | );
71 | } else {
72 | // Add skill
73 | const response = await axios.post(
74 | "https://udemy-tracker.vercel.app/skill",
75 | formData
76 | );
77 | setSkills([...skills, response.data]);
78 | }
79 | setShowModal(false);
80 | setFormData({ name: "", description: "", level: "", icon: "" });
81 | setCurrentSkill(null);
82 | } catch (err) {
83 | alert("Error saving skill. Please try again.");
84 | }
85 | };
86 |
87 | // Handle deleting a skill
88 | const handleDelete = async (id) => {
89 | // Retrieve password from localStorage
90 | const storedPassword = localStorage.getItem("password");
91 |
92 | // Check if the stored password matches the correct password
93 | if (storedPassword === correctPassword) {
94 | if (window.confirm("Are you sure you want to delete this skill?")) {
95 | try {
96 | await axios.delete(`https://udemy-tracker.vercel.app/skill/${id}`);
97 | setSkills(skills.filter((skill) => skill._id !== id));
98 | } catch (err) {
99 | alert("Error deleting skill. Please try again.");
100 | }
101 | }
102 | } else {
103 | alert(
104 | "⚠️ Access Denied: You lack authorization to perform this action. ⚠️"
105 | );
106 | }
107 | };
108 |
109 | // Open modal for adding or editing skill
110 | const openModal = (skill = null) => {
111 | // Retrieve password from localStorage
112 | const storedPassword = localStorage.getItem("password");
113 |
114 | // Check if the stored password matches the correct password
115 | if (storedPassword === correctPassword) {
116 | setCurrentSkill(skill);
117 | setFormData(skill || { name: "", description: "", level: "", icon: "" });
118 | setShowModal(true);
119 | } else {
120 | alert(
121 | "⚠️ Access Denied: You lack authorization to perform this action. ⚠️"
122 | );
123 | }
124 | };
125 |
126 | return (
127 |
132 |
133 |
👨🏻💻 Skills 👨🏻💻
134 |
135 |
136 |
openModal()}
141 | >
142 |
143 | Add Skill
144 |
145 |
146 |
147 | {/* Skills Section */}
148 | {loading ? (
149 |
152 | ) : (
153 | <>
154 |
155 | {skills.map((skill) => (
156 |
162 | {/* Delete button */}
163 |
handleDelete(skill._id)}
166 | >
167 |
168 |
169 |
170 | {/* Update button */}
171 |
openModal(skill)}
174 | >
175 |
176 |
177 |
178 |
183 | {renderIcon(skill.icon)}
184 |
185 |
{skill.name}
186 |
187 | {skill.description}
188 |
189 |
190 | {skill.level}
191 |
192 |
193 | ))}
194 |
195 | >
196 | )}
197 |
198 | {/* Modal */}
199 | {showModal && (
200 |
201 |
208 |
209 | {currentSkill ? "Update Skill" : "Add Skill"}
210 |
211 |
212 | Name
213 |
224 |
225 |
226 | Description
227 |
237 |
238 |
239 | Level
240 |
251 |
252 |
253 | Icon
254 |
265 |
266 |
267 | setShowModal(false)}
269 | className="px-4 py-2 rounded-md bg-gray-500 text-white"
270 | >
271 | Cancel
272 |
273 |
277 | Save
278 |
279 |
280 |
281 |
282 | )}
283 |
284 | );
285 | };
286 |
287 | export default Skills;
288 |
--------------------------------------------------------------------------------
/src/pages/Notes/EditNote.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams, useNavigate, Link } from "react-router-dom";
3 | import { ToastContainer, toast } from "react-toastify";
4 | import "react-toastify/dist/ReactToastify.css";
5 | import { useTheme } from "../../context/ThemeContext";
6 | import { fetchNoteById } from "../../dataService";
7 | import {
8 | categories as mainTargetCategories,
9 | targetGoals,
10 | subGoals as subTargetGoals,
11 | } from "../../db";
12 | import { Editor } from "@tinymce/tinymce-react";
13 |
14 | const EditNote = () => {
15 | const correctPassword = "12345";
16 | const { id } = useParams(); // Note ID for fetching/updating specific note
17 | const navigate = useNavigate();
18 | const { theme } = useTheme();
19 | const isDarkMode = theme === "dark";
20 | const [password, setPassword] = useState("");
21 | const [isAuthorized, setIsAuthorized] = useState(false);
22 |
23 | const [note, setNote] = useState({
24 | question: "",
25 | answer: "",
26 | mainTargetCategory: "",
27 | mainTargetGoal: "",
28 | subTargetGoal: "",
29 | });
30 |
31 | useEffect(() => {
32 | const fetchNoteDetails = async () => {
33 | try {
34 | const data = await fetchNoteById(id); // Call the service function to fetch the note data
35 | setNote({
36 | question: data.note?.question || "",
37 | answer: data.note?.answer || "",
38 | mainTargetCategory: data.note?.mainTargetCategory || "",
39 | mainTargetGoal: data.note?.mainTargetGoal || "",
40 | subTargetGoal: data.note?.subTargetGoal || "",
41 | });
42 | const storedPassword = localStorage.getItem("password");
43 | if (storedPassword === correctPassword) {
44 | setIsAuthorized(true);
45 | }
46 | } catch (error) {
47 | console.error("Error fetching note:", error);
48 | toast.error("Error fetching note data");
49 | }
50 | };
51 |
52 | if (id) {
53 | fetchNoteDetails(); // Fetch the note details when the component mounts or id changes
54 | }
55 | }, [id]);
56 |
57 | const handleChange = (e) => {
58 | const { name, value } = e.target;
59 | setNote((prevNote) => ({
60 | ...prevNote,
61 | [name]: value || "",
62 | }));
63 | };
64 |
65 | const handleCategoryChange = (e) => {
66 | setNote({
67 | ...note,
68 | mainTargetCategory: e.target.value || "",
69 | mainTargetGoal: "",
70 | subTargetGoal: "",
71 | });
72 | };
73 |
74 | const handleSubmit = async (e) => {
75 | e.preventDefault();
76 | try {
77 | const response = await fetch(
78 | `https://udemy-tracker.vercel.app/notes/update/${id}`,
79 | {
80 | method: "PUT",
81 | headers: { "Content-Type": "application/json" },
82 | body: JSON.stringify(note),
83 | }
84 | );
85 | if (!response.ok) throw new Error("Failed to update note");
86 |
87 | toast.success("Note updated successfully!");
88 | navigate(`/notes/${id}/view`);
89 | } catch (error) {
90 | console.error("Error updating note:", error);
91 | toast.error("Error updating note data");
92 | }
93 | };
94 |
95 | const handlePasswordSubmit = (e) => {
96 | e.preventDefault();
97 | const correctPassword = "12345";
98 | if (password === correctPassword) {
99 | setIsAuthorized(true);
100 | localStorage.setItem("password", password); // Store the password in localStorage
101 | toast.success("Access granted!");
102 | } else {
103 | toast.error("Incorrect password. Please try again.");
104 | }
105 | };
106 |
107 | const handleEditorChange = (content) => {
108 | setNote((prevNote) => ({
109 | ...prevNote,
110 | answer: content, // Update the answer with the editor content
111 | }));
112 | };
113 |
114 | return (
115 |
120 | {!isAuthorized ? (
121 |
127 |
128 | 🔒 Prove You're Worthy! Enter the Secret Code:
129 |
130 | setPassword(e.target.value)}
135 | className={`border p-2 rounded w-full ${
136 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
137 | }`}
138 | required
139 | />
140 |
144 | Submit
145 |
146 |
147 | ) : (
148 | <>
149 |
154 |
Edit Note
155 |
156 |
157 |
158 | Question:
159 |
160 |
173 |
174 |
175 |
176 | Answer:
177 |
178 |
206 |
207 |
208 |
209 | Main Target Category:
210 |
211 |
224 | Select a main target category
225 | {mainTargetCategories.map((category) => (
226 |
227 | {category}
228 |
229 | ))}
230 |
231 |
232 |
233 |
234 | Main Target Goal:
235 |
236 |
249 | Select a main target goal
250 | {note.mainTargetCategory &&
251 | targetGoals[note.mainTargetCategory]?.map((goal) => (
252 |
253 | {goal}
254 |
255 | ))}
256 |
257 |
258 |
259 |
260 | Sub Target Goal:
261 |
262 |
273 | Select a sub target goal
274 | {note.mainTargetGoal &&
275 | subTargetGoals[note.mainTargetGoal]?.map((subGoal) => (
276 |
277 | {subGoal}
278 |
279 | ))}
280 |
281 |
282 |
283 |
287 | Update Note
288 |
289 |
290 | Back to Notes
291 |
292 |
293 |
294 |
295 | >
296 | )}
297 |
298 |
299 | );
300 | };
301 |
302 | export default EditNote;
303 |
--------------------------------------------------------------------------------
/src/pages/Notes/ViewCourseNote.jsx:
--------------------------------------------------------------------------------
1 | // src/pages/ViewNoteForParticularCourse.js
2 | import React, { useState, useEffect } from "react";
3 | import { useParams, Link } from "react-router-dom";
4 | import { ToastContainer, toast } from "react-toastify";
5 | import "react-toastify/dist/ReactToastify.css";
6 | import { useTheme } from "../../context/ThemeContext";
7 |
8 | const ViewCourseNote = () => {
9 | const correctPassword = "12345";
10 | const { courseid, id } = useParams();
11 | const { theme } = useTheme();
12 | const isDarkMode = theme === "dark";
13 | const [note, setNote] = useState(null);
14 | const [editingNote, setEditingNote] = useState(null); // Track the note being edited
15 | const [question, setQuestion] = useState("");
16 | const [answer, setAnswer] = useState("");
17 | const [mainTargetCategory, setMainTargetCategory] = useState("");
18 | const [mainTargetGoal, setMainTargetGoal] = useState("");
19 | const [subTargetGoal, setSubTargetGoal] = useState("");
20 | const [password, setPassword] = useState("");
21 | const [isAuthorized, setIsAuthorized] = useState(false);
22 |
23 | useEffect(() => {
24 | const fetchNote = async () => {
25 | try {
26 | const response = await fetch(
27 | `https://udemy-tracker.vercel.app/notes/note/${id}`
28 | );
29 | if (!response.ok) {
30 | throw new Error("Failed to fetch note details");
31 | }
32 |
33 | const data = await response.json();
34 |
35 | // Set note state
36 | setNote(data.note);
37 |
38 | // Initialize form values with the fetched note data
39 | setQuestion(data.note.question || "");
40 | setAnswer(data.note.answer || "");
41 | setMainTargetCategory(data.note.mainTargetCategory || "");
42 | setMainTargetGoal(data.note.mainTargetGoal || "");
43 | setSubTargetGoal(data.note.subTargetGoal || "");
44 | const storedPassword = localStorage.getItem("password");
45 | if (storedPassword === correctPassword) {
46 | setIsAuthorized(true);
47 | }
48 | } catch (error) {
49 | console.error("Error fetching note:", error);
50 | toast.error("Error fetching note details");
51 | }
52 | };
53 |
54 | // Fetch note when component is mounted or when 'id' changes
55 | fetchNote();
56 | }, [id]);
57 |
58 | // Enable edit mode and pre-fill form with note details
59 | const handleEditClick = (note) => {
60 | setEditingNote(note._id);
61 | setQuestion(note.question);
62 | setAnswer(note.answer);
63 | setMainTargetCategory(note.mainTargetCategory);
64 | setMainTargetGoal(note.mainTargetGoal || ""); // Ensure it’s initialized
65 | setSubTargetGoal(note.subTargetGoal || ""); // Ensure it’s initialized
66 | };
67 |
68 | // Update a specific note
69 | const handleUpdate = async (e) => {
70 | e.preventDefault();
71 | try {
72 | const response = await fetch(
73 | `https://udemy-tracker.vercel.app/courses/${courseid}/notes/${editingNote}`,
74 | {
75 | method: "PUT",
76 | headers: {
77 | "Content-Type": "application/json",
78 | },
79 | body: JSON.stringify({
80 | question,
81 | answer,
82 | mainTargetCategory,
83 | mainTargetGoal,
84 | subTargetGoal,
85 | }),
86 | }
87 | );
88 | if (!response.ok) {
89 | throw new Error("Failed to update note");
90 | }
91 | const updatedNote = await response.json();
92 | setNote(updatedNote.note); // Update the displayed note details
93 | toast.success("Note updated successfully");
94 | setEditingNote(null); // Exit edit mode
95 | setQuestion("");
96 | setAnswer("");
97 | setMainTargetCategory("");
98 | setMainTargetGoal("");
99 | setSubTargetGoal("");
100 | } catch (error) {
101 | console.error("Error updating note:", error);
102 | toast.error("Failed to update note. Please try again later.");
103 | }
104 | };
105 |
106 | const handlePasswordSubmit = (e) => {
107 | e.preventDefault();
108 | const correctPassword = "12345";
109 | if (password === correctPassword) {
110 | setIsAuthorized(true);
111 | localStorage.setItem("password", password); // Store the password in localStorage
112 | toast.success("Access granted!");
113 | } else {
114 | toast.error("Incorrect password. Please try again.");
115 | }
116 | };
117 |
118 | return (
119 |
124 | {!isAuthorized ? (
125 |
131 |
132 | 🔒 Prove You're Worthy! Enter the Secret Code:
133 |
134 | setPassword(e.target.value)}
139 | className={`border p-2 rounded w-full ${
140 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
141 | }`}
142 | required
143 | />
144 |
148 | Submit
149 |
150 |
151 | ) : (
152 | <>
153 |
158 |
Note Details
159 |
160 | {editingNote ? (
161 |
162 |
163 | Question:
164 | setQuestion(e.target.value)}
168 | className={`w-full p-2 rounded border ${
169 | isDarkMode
170 | ? "bg-gray-700 text-white"
171 | : "bg-white text-black"
172 | }`}
173 | required
174 | />
175 |
176 |
177 | Answer:
178 | setAnswer(e.target.value)}
181 | className={`w-full p-2 rounded border ${
182 | isDarkMode
183 | ? "bg-gray-700 text-white"
184 | : "bg-white text-black"
185 | }`}
186 | required
187 | >
188 |
189 |
190 | Main Target Category:
191 | setMainTargetCategory(e.target.value)}
195 | className={`w-full p-2 rounded border ${
196 | isDarkMode
197 | ? "bg-gray-700 text-white"
198 | : "bg-white text-black"
199 | }`}
200 | required
201 | />
202 |
203 |
204 | Main Target Goal:
205 | setMainTargetGoal(e.target.value)}
209 | className={`w-full p-2 rounded border ${
210 | isDarkMode
211 | ? "bg-gray-700 text-white"
212 | : "bg-white text-black"
213 | }`}
214 | />
215 |
216 |
217 | Sub Target Goal:
218 | setSubTargetGoal(e.target.value)}
222 | className={`w-full p-2 rounded border ${
223 | isDarkMode
224 | ? "bg-gray-700 text-white"
225 | : "bg-white text-black"
226 | }`}
227 | />
228 |
229 |
230 |
238 | Save
239 |
240 | setEditingNote(false)}
243 | className="text-red-500"
244 | >
245 | Cancel
246 |
247 |
248 |
249 | ) : (
250 | <>
251 |
252 |
Question:
253 |
{note.question}
254 |
255 |
256 |
Answer:
257 |
258 |
259 |
260 |
261 | Main Target Category:
262 |
263 |
{note.mainTargetCategory}
264 |
265 |
266 |
Main Target Goal:
267 |
{note.mainTargetGoal}
268 |
269 |
270 |
Sub Target Goal:
271 |
{note.subTargetGoal}
272 |
273 |
handleEditClick(note)}
275 | className={`bg-blue-500 text-white p-2 rounded hover:bg-blue-600 mt-4`}
276 | >
277 | Edit Note
278 |
279 |
283 | Back to Course
284 |
285 | >
286 | )}
287 |
288 | >
289 | )}
290 |
291 |
292 | );
293 | };
294 |
295 | export default ViewCourseNote;
296 |
--------------------------------------------------------------------------------
/src/pages/Notes/EditNoteOfViewNotes.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useParams, useNavigate, Link } from "react-router-dom";
3 | import { ToastContainer, toast } from "react-toastify";
4 | import "react-toastify/dist/ReactToastify.css";
5 | import { useTheme } from "../../context/ThemeContext";
6 | import {
7 | categories as mainTargetCategories,
8 | targetGoals,
9 | subGoals as subTargetGoals,
10 | } from "../../db";
11 | import { Editor } from "@tinymce/tinymce-react";
12 |
13 | const EditNoteOfViewNotes = () => {
14 | const correctPassword = "12345";
15 | const { courseid, id } = useParams(); // Note ID for fetching/updating specific note
16 | const navigate = useNavigate();
17 | const { theme } = useTheme();
18 | const isDarkMode = theme === "dark";
19 | const [password, setPassword] = useState("");
20 | const [isAuthorized, setIsAuthorized] = useState(false);
21 |
22 | const [note, setNote] = useState({
23 | question: "",
24 | answer: "",
25 | mainTargetCategory: "",
26 | mainTargetGoal: "",
27 | subTargetGoal: "",
28 | });
29 |
30 | useEffect(() => {
31 | const fetchNote = async () => {
32 | try {
33 | const response = await fetch(
34 | `https://udemy-tracker.vercel.app/notes/note/${id}`
35 | );
36 |
37 | if (!response.ok) {
38 | throw new Error("Failed to fetch note data");
39 | }
40 |
41 | const data = await response.json();
42 |
43 | // Safely updating the note state
44 | setNote({
45 | question: data.note?.question || "",
46 | answer: data.note?.answer || "",
47 | mainTargetCategory: data.note?.mainTargetCategory || "",
48 | mainTargetGoal: data.note?.mainTargetGoal || "",
49 | subTargetGoal: data.note?.subTargetGoal || "",
50 | });
51 | const storedPassword = localStorage.getItem("password");
52 | if (storedPassword === correctPassword) {
53 | setIsAuthorized(true);
54 | }
55 | } catch (error) {
56 | console.error("Error fetching note:", error);
57 | toast.error("Error fetching note data");
58 | }
59 | };
60 |
61 | // Trigger fetch when `id` changes
62 | fetchNote();
63 | }, [id]);
64 |
65 | const handleChange = (e) => {
66 | const { name, value } = e.target;
67 | setNote((prevNote) => ({
68 | ...prevNote,
69 | [name]: value || "",
70 | }));
71 | };
72 |
73 | const handleCategoryChange = (e) => {
74 | setNote({
75 | ...note,
76 | mainTargetCategory: e.target.value || "",
77 | mainTargetGoal: "",
78 | subTargetGoal: "",
79 | });
80 | };
81 |
82 | const handleSubmit = async (e) => {
83 | e.preventDefault(); // Prevent the default form submission behavior
84 |
85 | try {
86 | // Ensure that the note object is correctly structured
87 | const response = await fetch(
88 | `https://udemy-tracker.vercel.app/notes/update/${id}`,
89 | {
90 | method: "PUT",
91 | headers: {
92 | "Content-Type": "application/json",
93 | },
94 | body: JSON.stringify(note), // Send the 'note' object as JSON
95 | }
96 | );
97 |
98 | // Check if the response is successful
99 | if (!response.ok) {
100 | throw new Error("Failed to update note");
101 | }
102 |
103 | // Show success message
104 | toast.success("Note updated successfully!");
105 |
106 | // Redirect to the view page after successful update
107 | navigate(`/courses/${courseid}/notes/note/${id}/view`);
108 | } catch (error) {
109 | // Log the error for debugging
110 | console.error("Error updating note:", error);
111 |
112 | // Show error message to the user
113 | toast.error("Error updating note data. Please try again.");
114 | }
115 | };
116 |
117 | const handlePasswordSubmit = (e) => {
118 | e.preventDefault();
119 | const correctPassword = "12345";
120 | if (password === correctPassword) {
121 | setIsAuthorized(true);
122 | localStorage.setItem("password", password); // Store the password in localStorage
123 | toast.success("Access granted!");
124 | } else {
125 | toast.error("Incorrect password. Please try again.");
126 | }
127 | };
128 |
129 | const handleEditorChange = (content) => {
130 | setNote((prevNote) => ({
131 | ...prevNote,
132 | answer: content, // Update the answer with the editor content
133 | }));
134 | };
135 |
136 | return (
137 |
142 | {!isAuthorized ? (
143 |
149 |
150 | 🔒 Prove You're Worthy! Enter the Secret Code:
151 |
152 | setPassword(e.target.value)}
157 | className={`border p-2 rounded w-full ${
158 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
159 | }`}
160 | required
161 | />
162 |
166 | Submit
167 |
168 |
169 | ) : (
170 | <>
171 |
176 |
Edit Note
177 |
178 |
179 |
180 | Question:
181 |
182 |
195 |
196 |
197 |
198 | Answer:
199 |
200 |
228 |
229 |
230 |
231 | Main Target Category:
232 |
233 |
246 | Select a main target category
247 | {mainTargetCategories.map((category) => (
248 |
249 | {category}
250 |
251 | ))}
252 |
253 |
254 |
255 |
256 | Main Target Goal:
257 |
258 |
271 | Select a main target goal
272 | {note.mainTargetCategory &&
273 | targetGoals[note.mainTargetCategory]?.map((goal) => (
274 |
275 | {goal}
276 |
277 | ))}
278 |
279 |
280 |
281 |
282 | Sub Target Goal:
283 |
284 |
295 | Select a sub target goal
296 | {note.mainTargetGoal &&
297 | subTargetGoals[note.mainTargetGoal]?.map((subGoal) => (
298 |
299 | {subGoal}
300 |
301 | ))}
302 |
303 |
304 |
305 |
309 | Update Note
310 |
311 |
315 | Cancel
316 |
317 |
321 | Back to Couse Notes
322 |
323 |
324 |
325 |
326 | >
327 | )}
328 |
329 |
330 | );
331 | };
332 |
333 | export default EditNoteOfViewNotes;
334 |
--------------------------------------------------------------------------------
/src/dataService.js:
--------------------------------------------------------------------------------
1 | // dataService.js
2 |
3 | const BASE_URL = "https://udemy-tracker.vercel.app/courses"; // Your API base URL
4 |
5 | // Helper function to get courses from local storage
6 | function getCoursesFromLocalStorage() {
7 | return JSON.parse(localStorage.getItem("courses")) || [];
8 | }
9 |
10 | // Helper function to save courses to local storage
11 | function saveCoursesToLocalStorage(courses) {
12 | localStorage.setItem("courses", JSON.stringify(courses));
13 | }
14 |
15 | // Function to update a note in local storage
16 | function updateNoteInLocalStorage(courseId, noteId, updatedNote) {
17 | let courses = getCoursesFromLocalStorage();
18 | const course = courses.find((course) => course._id === courseId);
19 |
20 | if (course) {
21 | const note = course.notes.find((note) => note._id === noteId);
22 | if (note) {
23 | note.question = updatedNote.question;
24 | note.answer = updatedNote.answer;
25 | saveCoursesToLocalStorage(courses);
26 | }
27 | }
28 | }
29 |
30 | // Function to sync the updated note with the backend
31 | async function syncNoteWithBackend(courseId, noteId, updatedNote) {
32 | const url = `${BASE_URL}/${courseId}/notes/${noteId}`;
33 |
34 | try {
35 | const response = await fetch(url, {
36 | method: "PUT",
37 | headers: {
38 | "Content-Type": "application/json",
39 | },
40 | body: JSON.stringify({
41 | question: updatedNote.question,
42 | answer: updatedNote.answer,
43 | }),
44 | });
45 | const data = await response.json();
46 | console.log("Sync successful:", data);
47 | return data;
48 | } catch (error) {
49 | console.error("Error syncing with backend:", error);
50 | throw error;
51 | }
52 | }
53 |
54 | // Function to auto-sync the note after 1 hour (3600000ms)
55 | function autoSyncNote(courseId, noteId, updatedNote) {
56 | // Save the updated note in local storage
57 | updateNoteInLocalStorage(courseId, noteId, updatedNote);
58 |
59 | // Set a timer to auto-sync after 1 hour
60 | setTimeout(() => {
61 | syncNoteWithBackend(courseId, noteId, updatedNote);
62 | }, 3600000); // 1 hour in milliseconds
63 | }
64 |
65 | // Function to create a new course and store it in local storage
66 | function createCourseInLocalStorage(course) {
67 | let courses = getCoursesFromLocalStorage();
68 | courses.push(course);
69 | saveCoursesToLocalStorage(courses);
70 | }
71 |
72 | // Function to create a new course and sync with backend
73 | async function createCourse(courseData) {
74 | try {
75 | const response = await fetch(BASE_URL, {
76 | method: "POST",
77 | headers: {
78 | "Content-Type": "application/json",
79 | },
80 | body: JSON.stringify(courseData),
81 | });
82 | const data = await response.json();
83 | // Save course in local storage
84 | createCourseInLocalStorage(data);
85 | return data;
86 | } catch (error) {
87 | console.error("Error creating course:", error);
88 | throw error;
89 | }
90 | }
91 |
92 | // Function to get all courses from backend
93 | async function getCoursesFromBackend() {
94 | try {
95 | const response = await fetch(BASE_URL);
96 | const data = await response.json();
97 | return data;
98 | } catch (error) {
99 | console.error("Error fetching courses:", error);
100 | throw error;
101 | }
102 | }
103 |
104 | // Function to get a specific course by ID from backend
105 | async function getCourseById(courseId) {
106 | const url = `${BASE_URL}/${courseId}`;
107 | try {
108 | const response = await fetch(url);
109 | const data = await response.json();
110 | return data;
111 | } catch (error) {
112 | console.error("Error fetching course:", error);
113 | throw error;
114 | }
115 | }
116 |
117 | // Function to update a course in local storage and sync with the backend
118 | async function updateCourse(courseId, courseData) {
119 | let courses = getCoursesFromLocalStorage();
120 | const courseIndex = courses.findIndex((course) => course._id === courseId);
121 |
122 | if (courseIndex !== -1) {
123 | // Update the course in local storage
124 | courses[courseIndex] = { ...courses[courseIndex], ...courseData };
125 | } else {
126 | // Add course only if it doesn't already exist
127 | courses.push({ _id: courseId, ...courseData });
128 | }
129 |
130 | // Remove any duplicate entries by creating a Set based on `_id`
131 | const uniqueCourses = Array.from(new Map(courses.map((course) => [course._id, course])).values());
132 |
133 | // Save the unique courses back to local storage
134 | saveCoursesToLocalStorage(uniqueCourses);
135 |
136 | // Sync the updated course with the backend
137 | const url = `${BASE_URL}/${courseId}`;
138 | try {
139 | const response = await fetch(url, {
140 | method: "PUT",
141 | headers: {
142 | "Content-Type": "application/json",
143 | },
144 | body: JSON.stringify(courseData),
145 | });
146 |
147 | if (!response.ok) {
148 | throw new Error("Failed to update course in backend");
149 | }
150 |
151 | const updatedCourse = await response.json();
152 |
153 | // Update local storage with the backend response to ensure consistency
154 | uniqueCourses[courseIndex] = updatedCourse;
155 | saveCoursesToLocalStorage(uniqueCourses);
156 |
157 | return updatedCourse;
158 | } catch (error) {
159 | console.error("Error updating course:", error);
160 | throw error;
161 | }
162 | }
163 |
164 |
165 | // Function to sync courses between local storage and the backend
166 | async function syncCoursesWithBackend() {
167 | try {
168 | // Fetch the latest courses from the backend
169 | const backendCourses = await getCoursesFromBackend();
170 |
171 | // Get the courses from local storage
172 | let localCourses = getCoursesFromLocalStorage();
173 |
174 | // Compare and update the local courses with the backend courses
175 | // First, keep the courses from the backend
176 | backendCourses.forEach((backendCourse) => {
177 | const index = localCourses.findIndex(
178 | (localCourse) => localCourse._id === backendCourse._id
179 | );
180 |
181 | // If the course exists locally, update it, otherwise add it to the local courses
182 | if (index !== -1) {
183 | localCourses[index] = backendCourse; // Update the course
184 | } else {
185 | localCourses.push(backendCourse); // Add the new course from backend
186 | }
187 | });
188 |
189 | // Now, remove any courses from local storage that no longer exist in the backend
190 | localCourses = localCourses.filter((localCourse) =>
191 | backendCourses.some(
192 | (backendCourse) => backendCourse._id === localCourse._id
193 | )
194 | );
195 |
196 | // Save the updated courses back to local storage
197 | saveCoursesToLocalStorage(localCourses);
198 |
199 | console.log("Courses successfully synced with the backend!");
200 | return localCourses;
201 | } catch (error) {
202 | console.error("Error syncing courses with backend:", error);
203 | throw error;
204 | }
205 | }
206 |
207 | // Function to fetch notes from the backend API
208 | export async function getNotesFromBackend() {
209 | try {
210 | const response = await fetch("https://udemy-tracker.vercel.app/notes/all");
211 | if (!response.ok) {
212 | throw new Error("Failed to fetch notes from the backend");
213 | }
214 |
215 | const data = await response.json();
216 | return data.notes; // Assuming the response structure contains 'notes'
217 | } catch (error) {
218 | console.error("Error fetching notes:", error);
219 | throw error;
220 | }
221 | }
222 | // Function to sync notes between local storage and the backend
223 | export async function syncNotesWithBackend() {
224 | try {
225 | // Fetch the latest notes from the backend
226 | const backendNotes = await getNotesFromBackend();
227 |
228 | // Get the notes from local storage
229 | let localNotes = getNotesFromLocalStorage();
230 |
231 | // Compare and update the local notes with the backend notes
232 | backendNotes.forEach((backendNote) => {
233 | const index = localNotes.findIndex(
234 | (localNote) => localNote._id === backendNote._id
235 | );
236 |
237 | // If the note exists locally, update it, otherwise add it to the local notes
238 | if (index !== -1) {
239 | localNotes[index] = backendNote; // Update the note
240 | } else {
241 | localNotes.push(backendNote); // Add the new note from backend
242 | }
243 | });
244 |
245 | // Now, remove any notes from local storage that no longer exist in the backend
246 | localNotes = localNotes.filter((localNote) =>
247 | backendNotes.some((backendNote) => backendNote._id === localNote._id)
248 | );
249 |
250 | // Save the updated notes back to local storage
251 | saveNotesToLocalStorage(localNotes);
252 |
253 | console.log("Notes successfully synced with the backend!");
254 | return localNotes;
255 | } catch (error) {
256 | console.error("Error syncing notes with backend:", error);
257 | throw error;
258 | }
259 | }
260 |
261 | // Function to update a note in the backend
262 | async function updateNote(courseId, noteId, updatedNote) {
263 | const url = `${BASE_URL}/${courseId}/notes/${noteId}`;
264 |
265 | const response = await fetch(url, {
266 | method: "PUT",
267 | headers: {
268 | "Content-Type": "application/json",
269 | },
270 | body: JSON.stringify(updatedNote),
271 | });
272 |
273 | if (!response.ok) {
274 | throw new Error("Failed to update note");
275 | }
276 |
277 | const data = await response.json();
278 | return data;
279 | }
280 |
281 | // Function to get notes from localStorage
282 | export const getNotesFromLocalStorage = () => {
283 | try {
284 | // Try to parse notes from localStorage
285 | const notes = JSON.parse(localStorage.getItem("notes"));
286 |
287 | // If notes exist in localStorage, return them; otherwise return an empty array
288 | return notes || [];
289 | } catch (error) {
290 | console.error("Error retrieving notes from localStorage:", error);
291 | return []; // Return an empty array if there was an error during parsing
292 | }
293 | };
294 |
295 | // Function to save notes to localStorage
296 | export const saveNotesToLocalStorage = (notes) => {
297 | try {
298 | // Save notes as a JSON string in localStorage
299 | localStorage.setItem("notes", JSON.stringify(notes));
300 | } catch (error) {
301 | console.error("Error saving notes to localStorage:", error);
302 | }
303 | };
304 |
305 | // Function to fetch the course name from the backend
306 | export const getCourseName = async (id) => {
307 | try {
308 | const response = await fetch(
309 | `https://udemy-tracker.vercel.app/courses/${id}`
310 | );
311 | if (!response.ok) {
312 | throw new Error("Failed to fetch course data");
313 | }
314 | const data = await response.json();
315 | return data.name; // Assuming 'name' is the property containing the course name
316 | } catch (error) {
317 | console.error("Error fetching course name:", error);
318 | throw error; // Optionally rethrow or return a default value
319 | }
320 | };
321 |
322 | // Function to add a note to a course
323 | export const addNoteToCourse = async (id, noteData) => {
324 | try {
325 | const response = await fetch(
326 | `https://udemy-tracker.vercel.app/courses/${id}/notes`,
327 | {
328 | method: "POST",
329 | headers: { "Content-Type": "application/json" },
330 | body: JSON.stringify(noteData),
331 | }
332 | );
333 | if (!response.ok) {
334 | throw new Error("Failed to add note");
335 | }
336 | return await response.json(); // Return response data if needed
337 | } catch (error) {
338 | console.error("Error adding note:", error);
339 | throw error; // Rethrow error for the calling function to handle
340 | }
341 | };
342 |
343 | // Function to fetch note details by id
344 | export const fetchNoteById = async (id) => {
345 | try {
346 | const response = await fetch(
347 | `https://udemy-tracker.vercel.app/notes/note/${id}`
348 | );
349 | if (!response.ok) {
350 | throw new Error("Failed to fetch note details");
351 | }
352 | return await response.json(); // Return the note data
353 | } catch (error) {
354 | console.error("Error fetching note:", error);
355 | throw error; // Rethrow the error to be handled in the component
356 | }
357 | };
358 |
359 | // Function to get course details from localStorage
360 | export const getCourseDetails = (courseId) => {
361 | try {
362 | const courses = JSON.parse(localStorage.getItem("courses")) || [];
363 | const course = courses.find((course) => course._id === courseId);
364 |
365 | if (!course) {
366 | throw new Error("Course not found in localStorage");
367 | }
368 |
369 | return {
370 | name: course.name, // Course name
371 | mainCategory: course.category, // Main category associated with the course
372 | targetGoal: course.subCategory, // Target goal (sub-category) associated with the course
373 | };
374 | } catch (error) {
375 | console.error("Error fetching course details from localStorage:", error);
376 | throw error;
377 | }
378 | };
379 |
380 | // Exporting functions to be used in your components
381 | export {
382 | getCoursesFromLocalStorage,
383 | saveCoursesToLocalStorage,
384 | updateNoteInLocalStorage,
385 | syncNoteWithBackend,
386 | autoSyncNote,
387 | createCourse,
388 | getCoursesFromBackend,
389 | getCourseById,
390 | updateCourse,
391 | syncCoursesWithBackend,
392 | updateNote,
393 | };
394 |
--------------------------------------------------------------------------------
/src/pages/Courses/AddCourse.jsx:
--------------------------------------------------------------------------------
1 | // src/pages/AddCourse.js
2 | import React, { useState, useEffect } from "react";
3 | import { ToastContainer, toast } from "react-toastify";
4 | import { Link } from "react-router-dom";
5 | import { useNavigate } from "react-router-dom";
6 | import "react-toastify/dist/ReactToastify.css";
7 | import { useTheme } from "../../context/ThemeContext"; // Import theme context
8 | import { createCourse } from "../../dataService";
9 | import {
10 | categories,
11 | categoryPriorities,
12 | targetGoals as subCategories,
13 | } from "../../db";
14 |
15 | const AddCourse = ({ onAdd }) => {
16 | const navigate = useNavigate();
17 | const correctPassword = "12345";
18 | const { theme } = useTheme(); // Use theme context
19 | const [password, setPassword] = useState("");
20 | const [isAuthorized, setIsAuthorized] = useState(false);
21 |
22 | const isDarkMode = theme === "dark"; // Check if dark mode is enabled
23 | const [courseData, setCourseData] = useState({
24 | no: "", // Make it blank and editable
25 | name: "",
26 | category: "",
27 | categoryPriority: "Medium priority",
28 | subCategory: "",
29 | subSubCategory: "",
30 | importantStatus: "Important",
31 | status: "Not Started Yet",
32 | durationInHours: "",
33 | subLearningSkillsSet: [],
34 | learningSkillsSet: "",
35 | });
36 |
37 | useEffect(() => {
38 | const storedPassword = localStorage.getItem("password");
39 | if (storedPassword === correctPassword) {
40 | setIsAuthorized(true);
41 | }
42 | }, []);
43 |
44 | const handlePasswordSubmit = (e) => {
45 | e.preventDefault();
46 | const correctPassword = "12345"; // Define the correct password here
47 | if (password === correctPassword) {
48 | setIsAuthorized(true);
49 | localStorage.setItem("password", password); // Store the password in localStorage
50 | toast.success("Access granted!");
51 | } else {
52 | toast.error("Incorrect password. Please try again.");
53 | }
54 | };
55 |
56 | const handleChange = (e) => {
57 | const { name, value } = e.target;
58 | setCourseData((prevData) => ({
59 | ...prevData,
60 | [name]:
61 | name === "subLearningSkillsSet"
62 | ? value.split(",").map((skill) => skill.trim()) // Convert comma-separated input into array
63 | : value,
64 | }));
65 | };
66 |
67 | const handleCategoryChange = (e) => {
68 | const selectedCategory = e.target.value;
69 | const priority = categoryPriorities[selectedCategory]; // Get priority from mapping
70 | setCourseData((prevData) => ({
71 | ...prevData,
72 | category: selectedCategory,
73 | categoryPriority: priority, // Automatically set priority
74 | subCategory: "", // Reset subcategory when category changes
75 | }));
76 | };
77 |
78 | const handleSubmit = async (e) => {
79 | e.preventDefault();
80 |
81 | // Prepare the course data with 'durationInHours' converted to a number
82 | const preparedData = {
83 | ...courseData,
84 | durationInHours: courseData.durationInHours
85 | ? parseFloat(courseData.durationInHours)
86 | : "",
87 | };
88 |
89 | try {
90 | // Call the createCourse function from dataService to handle the course creation
91 | const newCourse = await createCourse(preparedData);
92 |
93 | // On successful course creation, update the state with the new course
94 | onAdd(newCourse);
95 |
96 | // Show success toast
97 | toast.success("Course added successfully!", {
98 | position: "bottom-right",
99 | autoClose: 3000,
100 | });
101 |
102 | // Reset form after successful course addition
103 | setCourseData({
104 | no: "",
105 | name: "",
106 | category: "",
107 | categoryPriority: "",
108 | subCategory: "",
109 | subSubCategory: "",
110 | importantStatus: "",
111 | status: "",
112 | durationInHours: "",
113 | subLearningSkillsSet: [],
114 | learningSkillsSet: "",
115 | });
116 | navigate("/courses/");
117 | } catch (error) {
118 | // If there is an error, show the error toast
119 | console.error("Error adding course:", error);
120 | toast.error("Error adding course!", { position: "bottom-right" });
121 | }
122 | };
123 |
124 | return (
125 |
377 | );
378 | };
379 |
380 | export default AddCourse;
381 |
--------------------------------------------------------------------------------
/src/pages/Courses/EditCourse.jsx:
--------------------------------------------------------------------------------
1 | // src/pages/EditCourse.js
2 | import React, { useState, useEffect } from "react";
3 | import { useParams, useNavigate, Link } from "react-router-dom";
4 | import { ToastContainer, toast } from "react-toastify";
5 | import "react-toastify/dist/ReactToastify.css";
6 | import { useTheme } from "../../context/ThemeContext";
7 | import { updateCourse } from "../../dataService";
8 | import { categories, targetGoals as subCategories } from "../../db";
9 |
10 | const EditCourse = () => {
11 | const correctPassword = "12345";
12 | const { id } = useParams();
13 | const navigate = useNavigate();
14 | const { theme } = useTheme();
15 | const isDarkMode = theme === "dark";
16 | const [password, setPassword] = useState("");
17 | const [isAuthorized, setIsAuthorized] = useState(false);
18 |
19 | const [course, setCourse] = useState({
20 | no: "",
21 | name: "",
22 | category: "",
23 | categoryPriority: "Medium priority",
24 | subCategory: "",
25 | subSubCategory: "",
26 | importantStatus: "Important",
27 | status: "Not Started Yet",
28 | durationInHours: "",
29 | subLearningSkillsSet: [],
30 | learningSkillsSet: "",
31 | });
32 |
33 | useEffect(() => {
34 | const fetchCourse = async () => {
35 | // Check if the course data exists in localStorage
36 | const storedCourses = JSON.parse(localStorage.getItem("courses")) || [];
37 | const courseFromLocalStorage = storedCourses.find(
38 | (course) => course.id === id
39 | );
40 |
41 | if (courseFromLocalStorage) {
42 | // If the course is found in localStorage, use it
43 | setCourse(courseFromLocalStorage);
44 | } else {
45 | // If the course is not found, fetch it from the API
46 | try {
47 | const response = await fetch(
48 | `https://udemy-tracker.vercel.app/courses/${id}`
49 | );
50 | const data = await response.json();
51 | setCourse(data);
52 |
53 | // After fetching from the API, save it in localStorage
54 | storedCourses.push(data);
55 | localStorage.setItem("courses", JSON.stringify(storedCourses));
56 | const storedPassword = localStorage.getItem("password");
57 | if (storedPassword === correctPassword) {
58 | setIsAuthorized(true);
59 | }
60 | } catch (error) {
61 | console.error("Error fetching course:", error);
62 | toast.error("Error fetching course data");
63 | }
64 | }
65 | };
66 |
67 | fetchCourse();
68 | }, [id]);
69 |
70 | const handleChange = (e) => {
71 | const { name, value } = e.target;
72 | setCourse({ ...course, [name]: value });
73 | };
74 |
75 | const handleCategoryChange = (e) => {
76 | setCourse({ ...course, category: e.target.value, subCategory: "" });
77 | };
78 |
79 | const handleSkillsChange = (e) => {
80 | setCourse({
81 | ...course,
82 | subLearningSkillsSet: e.target.value
83 | .split(",")
84 | .map((skill) => skill.trim()),
85 | });
86 | };
87 |
88 | const handleSubmit = async (e) => {
89 | e.preventDefault();
90 | try {
91 | // Call the updateCourse function from dataService without assigning it to a variable
92 | await updateCourse(id, course);
93 |
94 | // Show success toast
95 | toast.success("Course updated successfully!");
96 |
97 | // Navigate to the course view page
98 | navigate(`/courses/${id}/view`);
99 | } catch (error) {
100 | console.error("Error updating course:", error);
101 | toast.error("Error updating course data");
102 | }
103 | };
104 |
105 | const handlePasswordSubmit = (e) => {
106 | e.preventDefault();
107 | if (password === correctPassword) {
108 | setIsAuthorized(true);
109 | localStorage.setItem("password", password); // Store the password in localStorage
110 | toast.success("Access granted!");
111 | } else {
112 | toast.error("Incorrect password. Please try again.");
113 | }
114 | };
115 |
116 | return (
117 |
122 | {!isAuthorized ? (
123 |
129 |
130 | 🔒 Prove You're Worthy! Enter the Secret Code:
131 |
132 | setPassword(e.target.value)}
138 | className={`border p-2 rounded w-full ${
139 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
140 | }`}
141 | required
142 | />
143 |
147 | Submit
148 |
149 |
150 | ) : (
151 | <>
152 |
396 |
397 | >
398 | )}
399 |
400 | );
401 | };
402 |
403 | export default EditCourse;
404 |
--------------------------------------------------------------------------------
/src/pages/Projects/Projects.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useTheme } from "../../context/ThemeContext";
3 | import axios from "axios";
4 | import ProjectModal from "./ProjectModal";
5 | import { AiOutlinePlus } from "react-icons/ai";
6 |
7 | const Projects = () => {
8 | const { theme } = useTheme();
9 | const isDarkMode = theme === "dark";
10 |
11 | const correctPassword = "12345";
12 | const [projects, setProjects] = useState([]);
13 | const [loading, setLoading] = useState(true);
14 | const [showAddModal, setShowAddModal] = useState(false);
15 | const [showUpdateModal, setShowUpdateModal] = useState(false);
16 | const [currentProject, setCurrentProject] = useState(null);
17 |
18 | const [currentProjectPage, setCurrentProjectPage] = useState(
19 | parseInt(localStorage.getItem("currentProjectPage")) || 1
20 | ); // Get the page from localStorage or default to 1
21 | const [projectsPerPage] = useState(6); // 6 projects per page
22 |
23 | const [categories, setCategories] = useState([]);
24 | const [selectedCategory, setSelectedCategory] = useState("");
25 | const [selectedSubCategory, setSelectedSubCategory] = useState("");
26 |
27 | // Update localStorage whenever projects or categories change
28 | useEffect(() => {
29 | const fetchProjects = async () => {
30 | try {
31 | setLoading(true);
32 | const response = await axios.get(
33 | "https://udemy-tracker.vercel.app/project"
34 | );
35 | setProjects(response.data);
36 | setLoading(false);
37 |
38 | // Get unique categories from projects
39 | const uniqueCategories = [
40 | ...new Set(response.data.map((project) => project.category)),
41 | ];
42 | setCategories(uniqueCategories);
43 | // Store the currentCertificatePage in localStorage
44 | localStorage.setItem("currentProjectPage", currentProjectPage);
45 | } catch (error) {
46 | console.error("Error fetching projects:", error);
47 | } finally {
48 | setLoading(false);
49 | }
50 | };
51 | fetchProjects();
52 |
53 | // Clear currentCertificatePage from localStorage on page reload
54 | const handlePageReload = () => {
55 | localStorage.removeItem("currentProjectPage");
56 | };
57 |
58 | window.addEventListener("beforeunload", handlePageReload);
59 |
60 | // Cleanup event listener on component unmount
61 | return () => {
62 | window.removeEventListener("beforeunload", handlePageReload);
63 | };
64 | }, [currentProjectPage]);
65 |
66 | // Add Project
67 | const addProject = async (newProject) => {
68 | try {
69 | const response = await axios.post(
70 | "https://udemy-tracker.vercel.app/project",
71 | newProject
72 | );
73 | setProjects([...projects, response.data]);
74 | setShowAddModal(false);
75 | } catch (error) {
76 | console.error("Error adding project:", error);
77 | }
78 | };
79 |
80 | // Update Project
81 | const updateProject = async (updatedProject) => {
82 | try {
83 | const response = await axios.put(
84 | `https://udemy-tracker.vercel.app/project/${updatedProject._id}`,
85 | updatedProject
86 | );
87 | setProjects(
88 | projects.map((project) =>
89 | project._id === updatedProject._id ? response.data : project
90 | )
91 | );
92 | setShowUpdateModal(false);
93 | } catch (error) {
94 | console.error("Error updating project:", error);
95 | }
96 | };
97 |
98 | // Delete Project with Confirmation
99 | const deleteProject = async (id) => {
100 | // Retrieve password from localStorage
101 | const storedPassword = localStorage.getItem("password");
102 |
103 | // Check if the stored password matches the correct password
104 | if (storedPassword === correctPassword) {
105 | const confirmDelete = window.confirm(
106 | "Are you sure you want to delete this project?"
107 | );
108 | if (confirmDelete) {
109 | try {
110 | await axios.delete(`https://udemy-tracker.vercel.app/project/${id}`);
111 | setProjects(projects.filter((project) => project._id !== id));
112 | } catch (error) {
113 | console.error("Error deleting project:", error);
114 | }
115 | }
116 | } else {
117 | alert(
118 | "⚠️ Access Denied: You lack authorization to perform this action. ⚠️"
119 | );
120 | }
121 | };
122 |
123 | // Filter projects based on selected category and subCategory
124 | const filteredProjects = projects.filter((project) => {
125 | const matchesCategory =
126 | !selectedCategory || project.category === selectedCategory;
127 | const matchesSubCategory =
128 | !selectedSubCategory || project.subCategory === selectedSubCategory;
129 |
130 | return matchesCategory && matchesSubCategory;
131 | });
132 |
133 | // Handle category and sub-category change
134 | const handleCategoryChange = (e) => {
135 | const category = e.target.value;
136 | setSelectedCategory(category);
137 | setSelectedSubCategory(""); // Reset sub-category when category changes
138 | };
139 |
140 | const handleSubCategoryChange = (e) => {
141 | setSelectedSubCategory(e.target.value);
142 | };
143 |
144 | // Get unique sub-categories based on selected category
145 | const filteredSubCategories = [
146 | ...new Set(
147 | projects
148 | .filter((project) => project.category === selectedCategory)
149 | .map((project) => project.subCategory)
150 | ),
151 | ];
152 |
153 | // Pagination logic
154 | const indexOfLastProject = currentProjectPage * projectsPerPage;
155 | const indexOfFirstProject = indexOfLastProject - projectsPerPage;
156 | const currentProjects = filteredProjects.slice(
157 | indexOfFirstProject,
158 | indexOfLastProject
159 | );
160 |
161 | const totalPages = Math.ceil(filteredProjects.length / projectsPerPage);
162 |
163 | // Function to handle page change and scroll to top
164 | const handlePageChange = (newPage) => {
165 | setCurrentProjectPage(newPage);
166 | window.scrollTo(0, 0); // Scroll to the top of the page
167 | };
168 |
169 | return (
170 |
175 |
176 | 💼 Projects 💼
177 |
178 |
179 | {/* Filters Section */}
180 |
181 | {/* Category Filter */}
182 |
191 | All Categories
192 | {categories.map((category, idx) => (
193 |
194 | {category}
195 |
196 | ))}
197 |
198 |
199 | {/* Sub-category Filter */}
200 |
210 | All Sub-Categories
211 | {filteredSubCategories.map((subCategory, idx) => (
212 |
213 | {subCategory}
214 |
215 | ))}
216 |
217 |
218 |
219 | {/* Add Project Button */}
220 |
221 |
{
223 | const storedPassword = localStorage.getItem("password");
224 |
225 | if (correctPassword === storedPassword) {
226 | setShowAddModal(true);
227 | } else {
228 | alert(
229 | "⚠️ Access Denied: You lack authorization to perform this action. ⚠️"
230 | );
231 | }
232 | }}
233 | className={`rounded-md h-10 w-full sm:w-32 transition duration-200 flex items-center justify-center ${
234 | isDarkMode
235 | ? "bg-blue-600 hover:bg-blue-700 text-white"
236 | : "bg-blue-500 hover:bg-blue-600 text-white"
237 | }`}
238 | >
239 |
240 | Add Project
241 |
242 |
243 |
244 | {/* Projects Section */}
245 | {loading ? (
246 |
249 | ) : filteredProjects.length > 0 ? (
250 | <>
251 |
252 | {currentProjects.map((project) => (
253 |
261 | {/* Title */}
262 |
267 | {project.title}
268 |
269 |
270 | {/* Category and Sub-Category */}
271 |
272 |
273 | Category: {project.category}
274 |
275 |
276 | Sub-Category: {project.subCategory}
277 |
278 |
279 |
280 | {/* Description */}
281 |
{project.description}
282 |
283 | {/* Tech Stack */}
284 |
285 |
Tech Stack:
286 |
287 | {project.tech.map((tech, idx) => (
288 |
296 | {tech}
297 |
298 | ))}
299 |
300 |
301 |
302 | {/* Links */}
303 |
353 |
354 | {/* Action Buttons */}
355 |
356 | {
358 | const storedPassword = localStorage.getItem("password");
359 | if (correctPassword === storedPassword) {
360 | setCurrentProject(project);
361 | setShowUpdateModal(true);
362 | } else {
363 | alert(
364 | "⚠️ Access Denied: You lack authorization to perform this action. ⚠️"
365 | );
366 | }
367 | }}
368 | className="w-full py-2 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg shadow-sm transition-colors"
369 | >
370 | Update
371 |
372 | deleteProject(project._id)}
374 | className="w-full py-2 bg-red-500 hover:bg-red-600 text-white font-medium rounded-lg shadow-sm transition-colors"
375 | >
376 | Delete
377 |
378 |
379 |
380 | ))}
381 |
382 | >
383 | ) : (
384 |
No projects available.
385 | )}
386 |
387 | {/* Pagination Controls */}
388 |
389 | handlePageChange(currentProjectPage - 1)}
391 | disabled={currentProjectPage === 1}
392 | className={`p-2 rounded ${
393 | isDarkMode
394 | ? "bg-gray-700 text-white hover:bg-gray-600"
395 | : "bg-gray-300 text-black hover:bg-gray-400"
396 | }`}
397 | >
398 | Previous
399 |
400 |
401 | Page {currentProjectPage} of {totalPages}
402 |
403 | handlePageChange(currentProjectPage + 1)}
405 | disabled={currentProjectPage === totalPages}
406 | className={`p-2 rounded ${
407 | isDarkMode
408 | ? "bg-gray-700 text-white hover:bg-gray-600"
409 | : "bg-gray-300 text-black hover:bg-gray-400"
410 | }`}
411 | >
412 | Next
413 |
414 |
415 |
416 | {/* Modals */}
417 | {showAddModal && (
418 |
setShowAddModal(false)}
420 | onSubmit={addProject}
421 | />
422 | )}
423 | {showUpdateModal && (
424 | setShowUpdateModal(false)}
427 | onSubmit={updateProject}
428 | />
429 | )}
430 |
431 | );
432 | };
433 |
434 | export default Projects;
435 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Bar } from "react-chartjs-2";
3 | import { useTheme } from "../../context/ThemeContext";
4 | import {
5 | getCoursesFromLocalStorage,
6 | getCoursesFromBackend,
7 | } from "../../dataService";
8 | import "./Home.css";
9 |
10 | import {
11 | Chart as ChartJS,
12 | CategoryScale,
13 | LinearScale,
14 | BarElement,
15 | Tooltip,
16 | Legend,
17 | } from "chart.js";
18 | import { Link } from "react-router-dom";
19 |
20 | ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend);
21 |
22 | const Home = () => {
23 | const { theme } = useTheme();
24 | const isDarkMode = theme === "dark";
25 |
26 | const [courses, setCourses] = useState([]);
27 | const [completedCoursesCount, setCompletedCoursesCount] = useState(0);
28 | const [selectedCategory, setSelectedCategory] = useState("");
29 | const [selectedSubCategory, setSelectedSubCategory] = useState("");
30 | const [selectedImportantStatus, setSelectedImportantStatus] = useState("");
31 | const [selectedStatus, setSelectedStatus] = useState("");
32 | const [isLoading, setIsLoading] = useState(true);
33 |
34 | useEffect(() => {
35 | // Function to fetch courses from localStorage
36 | const fetchCourses = () => {
37 | setIsLoading(true);
38 | try {
39 | // Get courses from localStorage
40 | const storedCourses = getCoursesFromLocalStorage();
41 |
42 | if (storedCourses) {
43 | // If courses are in localStorage, use them
44 | setCourses(storedCourses);
45 |
46 | // Count completed courses
47 | const completedCount = storedCourses.filter(
48 | (course) => course.status === "Completed"
49 | ).length;
50 | setCompletedCoursesCount(completedCount);
51 | }
52 | setIsLoading(false); // Set loading to false once fetching is complete
53 | } catch (error) {
54 | console.error("Error fetching courses from localStorage:", error);
55 | setIsLoading(false); // Stop loading even if there's an error
56 | }
57 | };
58 |
59 | fetchCourses(); // Call fetchCourses to load data from localStorage
60 | }, []);
61 |
62 | useEffect(() => {
63 | const fetchCourses = async () => {
64 | setIsLoading(true);
65 | try {
66 | // Check if courses are already in localStorage
67 | const storedCourses = localStorage.getItem("courses");
68 |
69 | if (storedCourses) {
70 | // If courses are found in localStorage, use them
71 | setCourses(JSON.parse(storedCourses));
72 | } else {
73 | // If no courses in localStorage, fetch from backend
74 | const data = await getCoursesFromBackend();
75 |
76 | // Sort courses by the 'no' field
77 | const sortedCourses = data.sort((a, b) => a.no - b.no);
78 |
79 | // Store the fetched courses in localStorage
80 | localStorage.setItem("courses", JSON.stringify(sortedCourses));
81 |
82 | // Set the courses in state
83 | setCourses(sortedCourses);
84 |
85 | // Count completed courses
86 | const completedCount = sortedCourses.filter(
87 | (course) => course.status === "Completed"
88 | ).length;
89 | setCompletedCoursesCount(completedCount);
90 | }
91 | } catch (error) {
92 | console.error("Error fetching courses:", error);
93 | } finally {
94 | setIsLoading(false);
95 | }
96 | };
97 |
98 | fetchCourses();
99 | }, []);
100 |
101 | const groupBy = (array, key) => {
102 | return array.reduce((acc, item) => {
103 | const value = item[key] || "Unknown";
104 | acc[value] = (acc[value] || 0) + 1;
105 | return acc;
106 | }, {});
107 | };
108 |
109 | const getFilteredData = () => {
110 | return courses.filter(
111 | (course) =>
112 | (selectedCategory ? course.category === selectedCategory : true) &&
113 | (selectedSubCategory
114 | ? course.subCategory === selectedSubCategory
115 | : true) &&
116 | (selectedImportantStatus
117 | ? course.importantStatus === selectedImportantStatus
118 | : true) &&
119 | (selectedStatus ? course.status === selectedStatus : true)
120 | );
121 | };
122 |
123 | const getChartData = (key, filteredCourses) => {
124 | const groupedData = groupBy(filteredCourses, key);
125 | return {
126 | labels: Object.keys(groupedData),
127 | datasets: [
128 | {
129 | label: `Courses by ${key.charAt(0).toUpperCase() + key.slice(1)}`,
130 | data: Object.values(groupedData),
131 | backgroundColor: isDarkMode
132 | ? ["#FF6384", "#36A2EB", "#FFCE56", "#66BB6A"]
133 | : ["#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0"],
134 | borderWidth: 1,
135 | },
136 | ],
137 | };
138 | };
139 |
140 | const categoryOptions = {
141 | plugins: {
142 | legend: {
143 | display: false,
144 | },
145 | },
146 | scales: {
147 | x: {
148 | ticks: {
149 | color: isDarkMode ? "#FFFFFF" : "#000000",
150 | },
151 | },
152 | y: {
153 | ticks: {
154 | color: isDarkMode ? "#FFFFFF" : "#000000",
155 | },
156 | },
157 | },
158 | maintainAspectRatio: false,
159 | };
160 |
161 | const categories = [...new Set(courses.map((course) => course.category))];
162 | const subCategories = [
163 | ...new Set(getFilteredData().map((course) => course.subCategory)),
164 | ];
165 | const importanceStatuses = [
166 | ...new Set(courses.map((course) => course.importantStatus)),
167 | ];
168 | const statuses = [...new Set(courses.map((course) => course.status))];
169 |
170 | const selectClassName = `p-2 rounded-md shadow-md ${
171 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
172 | }`;
173 |
174 | const filteredData = getFilteredData();
175 |
176 | return (
177 |
182 | {isLoading ? (
183 |
186 | ) : (
187 | <>
188 |
189 |
190 | Udemy Courses Analysis - Getting Start
191 |
192 |
193 |
194 |
195 |
196 | 📚 Courses 📚
197 |
198 |
199 |
200 |
201 | 📝 Notes 📝
202 |
203 |
204 |
205 |
206 | 👨🏻💻 Skills 👨🏻💻
207 |
208 |
209 |
210 |
211 | 💼 Projects 💼
212 |
213 |
214 |
215 |
216 | 📈 Progress 📈
217 |
218 |
219 |
220 |
221 | 🙎♂️ Profile 🙎♂️
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | 🏆 Certificates 🏆
230 |
231 |
232 |
233 |
234 |
235 |
236 |
241 |
Total Courses
242 |
{courses.length}
243 |
244 |
249 |
Completed Courses
250 |
251 | {completedCoursesCount}
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 | Select Category:
261 |
262 | {
267 | setSelectedCategory(e.target.value);
268 | setSelectedSubCategory("");
269 | }}
270 | >
271 | All Categories
272 | {categories.map((category) => (
273 |
274 | {category}
275 |
276 | ))}
277 |
278 |
279 |
280 |
281 |
282 | Select Sub-Category:
283 |
284 | setSelectedSubCategory(e.target.value)}
289 | >
290 | All Sub-Categories
291 | {subCategories.map((subCategory) => (
292 |
293 | {subCategory}
294 |
295 | ))}
296 |
297 |
298 |
299 |
300 |
304 | Select Important Status:
305 |
306 | setSelectedImportantStatus(e.target.value)}
311 | >
312 | All Importance
313 | {importanceStatuses.map((status) => (
314 |
315 | {status}
316 |
317 | ))}
318 |
319 |
320 |
321 |
322 |
323 | Select Status:
324 |
325 | setSelectedStatus(e.target.value)}
330 | >
331 | All Status
332 | {statuses.map((status) => (
333 |
334 | {status}
335 |
336 | ))}
337 |
338 |
339 |
340 |
341 |
342 |
347 |
Category Count
348 |
349 | {
350 | filteredData.filter(
351 | (course) => course.category === selectedCategory
352 | ).length
353 | }
354 |
355 |
356 |
357 |
362 |
Sub-Category Count
363 |
364 | {
365 | filteredData.filter(
366 | (course) => course.subCategory === selectedSubCategory
367 | ).length
368 | }
369 |
370 |
371 |
372 |
377 |
Important Status Count
378 |
379 | {
380 | filteredData.filter(
381 | (course) =>
382 | course.importantStatus === selectedImportantStatus
383 | ).length
384 | }
385 |
386 |
387 |
388 |
393 |
Status Count
394 |
395 | {
396 | filteredData.filter(
397 | (course) => course.status === selectedStatus
398 | ).length
399 | }
400 |
401 |
402 |
403 |
404 |
405 |
406 |
410 |
411 | Courses by Category
412 |
413 |
414 |
415 |
419 |
420 | Courses by Sub-Category
421 |
422 |
423 |
424 |
428 |
429 | Courses by Importance
430 |
431 |
432 |
433 |
437 |
438 | Courses by Status
439 |
440 |
441 |
442 | >
443 | )}
444 |
445 | );
446 | };
447 |
448 | export default Home;
449 |
--------------------------------------------------------------------------------
/src/pages/Certificate/Certificate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useTheme } from "../../context/ThemeContext";
3 | import { toast } from "react-toastify";
4 | import "react-toastify/dist/ReactToastify.css";
5 |
6 | const Certificates = () => {
7 | const { theme } = useTheme(); // Access theme from ThemeContext
8 | const isDarkMode = theme === "dark";
9 |
10 | const correctPassword = "12345";
11 | const [loading, setLoading] = useState(true);
12 | const [certificates, setCertificates] = useState([]);
13 | const [searchTerm, setSearchTerm] = useState("");
14 | const [filteredCertificates, setFilteredCertificates] = useState([]);
15 | const [currentCerticatePage, setCurrentCerticatePage] = useState(
16 | parseInt(localStorage.getItem("currentCerticatePage")) || 1
17 | ); // Get the page from localStorage or default to 1
18 | const [certificatesPerPage] = useState(8); // 8 certificates per page
19 | const [isModalOpen, setIsModalOpen] = useState(false); // Modal state
20 | const [newCertificate, setNewCertificate] = useState({
21 | imageUrl: "",
22 | courseName: "",
23 | });
24 | const [password, setPassword] = useState("");
25 | const [isAuthorized, setIsAuthorized] = useState(false);
26 |
27 | useEffect(() => {
28 | // Fetch certificates from backend
29 | const fetchCertificates = async () => {
30 | try {
31 | const response = await fetch(
32 | "https://udemy-tracker.vercel.app/certificate"
33 | );
34 | const data = await response.json();
35 | setCertificates(data.certificates);
36 | setFilteredCertificates(data.certificates);
37 |
38 | const storedPassword = localStorage.getItem("password");
39 | if (storedPassword === correctPassword) {
40 | setIsAuthorized(true);
41 | }
42 | // Store the currentCertificatePage in localStorage
43 | localStorage.setItem("currentCerticatePage", currentCerticatePage);
44 | } catch (error) {
45 | console.error("Error fetching certificates:", error);
46 | } finally {
47 | setLoading(false);
48 | }
49 | };
50 |
51 | fetchCertificates();
52 |
53 | // Clear currentCertificatePage from localStorage on page reload
54 | const handlePageReload = () => {
55 | localStorage.removeItem("currentCerticatePage");
56 | };
57 |
58 | window.addEventListener("beforeunload", handlePageReload);
59 |
60 | // Cleanup event listener on component unmount
61 | return () => {
62 | window.removeEventListener("beforeunload", handlePageReload);
63 | };
64 | }, [currentCerticatePage]);
65 |
66 | // Filter certificates based on search term
67 | useEffect(() => {
68 | if (searchTerm === "") {
69 | setFilteredCertificates(certificates);
70 | } else {
71 | const filtered = certificates.filter((certificate) =>
72 | certificate.courseName.toLowerCase().includes(searchTerm.toLowerCase())
73 | );
74 | setFilteredCertificates(filtered);
75 | setCurrentCerticatePage(1); // Reset to the first page on search
76 | }
77 | }, [searchTerm, certificates]);
78 |
79 | // Handle form submission for new certificate
80 | const handleAddCertificate = async () => {
81 | if (!newCertificate.imageUrl || !newCertificate.courseName) {
82 | alert("Please fill out all fields.");
83 | return;
84 | }
85 |
86 | try {
87 | const response = await fetch(
88 | "https://udemy-tracker.vercel.app/certificate",
89 | {
90 | method: "POST",
91 | headers: { "Content-Type": "application/json" },
92 | body: JSON.stringify(newCertificate),
93 | }
94 | );
95 |
96 | const data = await response.json();
97 | setCertificates([...certificates, data.certificate]);
98 | setFilteredCertificates([...filteredCertificates, data.certificate]);
99 | setNewCertificate({ imageUrl: "", courseName: "" });
100 | setIsModalOpen(false); // Close modal
101 | } catch (error) {
102 | console.error("Error adding certificate:", error);
103 | }
104 | };
105 |
106 | // Handle Delete Function
107 | const handleDelete = async (certificateId) => {
108 | // Retrieve password from localStorage
109 | const storedPassword = localStorage.getItem("password");
110 |
111 | // Check if the stored password matches the correct password
112 | if (storedPassword === correctPassword) {
113 | if (window.confirm("Are you sure you want to delete this certificate?")) {
114 | try {
115 | // API call to delete the certificate
116 | await fetch(
117 | `https://udemy-tracker.vercel.app/certificate/${certificateId}`,
118 | {
119 | method: "DELETE",
120 | }
121 | );
122 |
123 | // Update the certificates state after deletion
124 | const updatedCertificates = certificates.filter(
125 | (certificate) => certificate._id !== certificateId
126 | );
127 | setCertificates(updatedCertificates);
128 | } catch (error) {
129 | console.error("Error deleting certificate:", error);
130 | }
131 | }
132 | } else {
133 | alert(
134 | "⚠️ Access Denied: You lack authorization to perform this action. ⚠️"
135 | );
136 | }
137 | };
138 |
139 | // Pagination logic
140 | const indexOfLastCertificate = currentCerticatePage * certificatesPerPage;
141 | const indexOfFirstCertificate = indexOfLastCertificate - certificatesPerPage;
142 | const currentCertificates = filteredCertificates.slice(
143 | indexOfFirstCertificate,
144 | indexOfLastCertificate
145 | );
146 |
147 | const totalPages = Math.ceil(
148 | filteredCertificates.length / certificatesPerPage
149 | );
150 |
151 | // Function to handle page change and scroll to top
152 | const handlePageChange = (newPage) => {
153 | setCurrentCerticatePage(newPage);
154 | window.scrollTo(0, 0); // Scroll to the top of the page
155 | };
156 |
157 | const handlePasswordSubmit = (e) => {
158 | e.preventDefault();
159 | if (password === correctPassword) {
160 | setIsAuthorized(true);
161 | localStorage.setItem("password", password);
162 | toast.success("Access granted!");
163 | } else {
164 | toast.error("Incorrect password. Please try again.");
165 | }
166 | };
167 |
168 | return (
169 |
174 |
179 | 🏆 Certificates 🏆
180 |
181 | {/* Header with Search and Add Button */}
182 |
183 | {/* Search Bar */}
184 | setSearchTerm(e.target.value)}
194 | />
195 |
196 | {/* Add Certificate Button */}
197 | {
200 | if (!isAuthorized) {
201 | setIsModalOpen(true);
202 | } else {
203 | setIsModalOpen(true); // Directly open the modal if authorized
204 | }
205 | }}
206 | >
207 | Add Certificate
208 |
209 |
210 |
211 | {/* Certificates Grid */}
212 | {loading ? (
213 |
216 | ) : (
217 | <>
218 |
219 | {currentCertificates.map((certificate) => (
220 |
228 | {/* Certificate Image */}
229 |
235 |
236 | {/* Certificate Details */}
237 |
238 |
243 | {certificate.courseName}
244 |
245 |
246 | {/* Action Buttons */}
247 |
248 | {/* View Button */}
249 |
259 | View
260 |
261 |
262 | {/* Delete Button */}
263 |
handleDelete(certificate._id)}
265 | className={`px-3 py-1 text-sm rounded-md font-medium transition-all duration-300 ${
266 | isDarkMode
267 | ? "bg-red-600 text-white hover:bg-red-500"
268 | : "bg-red-500 text-white hover:bg-red-600"
269 | }`}
270 | >
271 | Delete
272 |
273 |
274 |
275 |
276 | ))}
277 |
278 | >
279 | )}
280 |
281 | {/* Pagination Controls */}
282 |
283 | handlePageChange(currentCerticatePage - 1)}
285 | disabled={currentCerticatePage === 1}
286 | className={`p-2 rounded ${
287 | isDarkMode
288 | ? "bg-gray-700 text-white hover:bg-gray-600"
289 | : "bg-gray-300 text-black hover:bg-gray-400"
290 | }`}
291 | >
292 | Previous
293 |
294 |
295 | Page {currentCerticatePage} of {totalPages}
296 |
297 | handlePageChange(currentCerticatePage + 1)}
299 | disabled={currentCerticatePage === totalPages}
300 | className={`p-2 rounded ${
301 | isDarkMode
302 | ? "bg-gray-700 text-white hover:bg-gray-600"
303 | : "bg-gray-300 text-black hover:bg-gray-400"
304 | }`}
305 | >
306 | Next
307 |
308 |
309 |
310 | {/* Add Certificate Modal */}
311 | {isModalOpen && (
312 |
315 | {!isAuthorized ? (
316 |
322 |
323 | 🔒 Prove You're Worthy! Enter the Secret Code:
324 |
325 | setPassword(e.target.value)}
331 | className={`border p-2 rounded w-full ${
332 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black"
333 | }`}
334 | required
335 | />
336 |
340 | Submit
341 |
342 | setIsModalOpen(false)}
345 | >
346 | Cancel
347 |
348 |
349 | ) : (
350 |
355 | {/* Modal Title */}
356 |
361 | Add Certificate
362 |
363 |
364 | {/* Image URL Input */}
365 |
366 |
371 | Image URL
372 |
373 |
382 | setNewCertificate({
383 | ...newCertificate,
384 | imageUrl: e.target.value,
385 | })
386 | }
387 | autoFocus
388 | />
389 |
390 |
391 | {/* Course Name Input */}
392 |
393 |
398 | Course Name
399 |
400 |
409 | setNewCertificate({
410 | ...newCertificate,
411 | courseName: e.target.value,
412 | })
413 | }
414 | />
415 |
416 |
417 | {/* Action Buttons */}
418 |
419 | setIsModalOpen(false)}
426 | >
427 | Cancel
428 |
429 |
437 | Submit
438 |
439 |
440 |
441 | )}
442 |
443 | )}
444 |
445 | );
446 | };
447 |
448 | export default Certificates;
449 |
--------------------------------------------------------------------------------