├── 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 | 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 |
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 |
89 | 92 | setPassword(e.target.value)} 97 | className={`border p-2 rounded w-full ${ 98 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black" 99 | }`} 100 | required 101 | /> 102 | 108 |
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 | 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 |
99 | 102 | setPassword(e.target.value)} 107 | className={`border p-2 rounded w-full ${ 108 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black" 109 | }`} 110 | required 111 | /> 112 | 118 |
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 | 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 | 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 |
90 | {" "} 91 | {/* Scrollable form content */} 92 |
93 | 96 | 109 |
110 |
111 | 117 | 188 |
189 |
190 | 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 | 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 | 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 | 240 | 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 | 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 | 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 | 168 |
169 | ) : ( 170 | <> 171 |
176 |

Edit Note

177 |
178 |
179 | 182 | 195 |
196 |
197 | 200 | 228 |
229 |
230 | 233 | 253 |
254 |
255 | 258 | 279 |
280 |
281 | 284 | 303 |
304 |
305 | 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 |
130 | {!isAuthorized ? ( 131 |
137 | 140 | setPassword(e.target.value)} 146 | className={`border p-2 rounded w-full ${ 147 | isDarkMode ? "bg-gray-700 text-white" : "bg-white text-black" 148 | }`} 149 | required 150 | /> 151 | 157 |
158 | ) : ( 159 | <> 160 |
166 |

Add New Course

167 |
168 | 171 | 181 |
182 |
183 | 186 | 197 |
198 |
199 | 202 | 219 |
220 |
221 | 224 | 242 |
243 |
244 | 247 | 257 |
258 |
259 | 262 | 276 |
277 |
278 | 281 | 295 |
296 |
297 | 300 | 313 |
314 |
315 | 318 | 328 |
329 |
330 | 333 | 343 |
344 |
345 | 348 | 358 |
359 |
360 | 366 |
367 | 368 | Cancel 369 | 370 |
371 |
372 |
373 | 374 | 375 | )} 376 |
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 | 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 | 149 |
150 | ) : ( 151 | <> 152 |
157 |

Edit Course

158 |
159 |
160 | 163 | 176 |
177 |
178 | 181 | 194 |
195 |
196 | 199 | 218 |
219 |
220 | 223 | 243 |
244 |
245 | 248 | 260 |
261 |
262 | 265 | 281 |
282 |
283 | 286 | 302 |
303 |
304 | 307 | 322 |
323 |
324 | 327 | 339 |
340 |
341 | 344 | 356 |
357 |
358 | 361 | 373 |
374 |
375 | 381 | 385 | Cancel 386 | 387 | 391 | Back to Courses 392 | 393 |
394 |
395 |
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 | 198 | 199 | {/* Sub-category Filter */} 200 | 217 |
218 | 219 | {/* Add Project Button */} 220 |
221 | 242 |
243 | 244 | {/* Projects Section */} 245 | {loading ? ( 246 |
247 |
248 |
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 | 372 | 378 |
379 |
380 | ))} 381 |
382 | 383 | ) : ( 384 |
No projects available.
385 | )} 386 | 387 | {/* Pagination Controls */} 388 |
389 | 400 | 401 | Page {currentProjectPage} of {totalPages} 402 | 403 | 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 |
184 |
185 |
186 | ) : ( 187 | <> 188 |
189 |

190 | Udemy Courses Analysis - Getting Start 191 |

192 |
193 |
194 | 195 | 198 | 199 | 200 | 203 | 204 | 205 | 208 | 209 | 210 | 213 | 214 | 215 | 218 | 219 | 220 | 223 | 224 |
225 | 226 |
227 | 228 | 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 | 262 | 278 |
279 | 280 |
281 | 284 | 297 |
298 | 299 |
300 | 306 | 319 |
320 | 321 |
322 | 325 | 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 | 209 |
210 | 211 | {/* Certificates Grid */} 212 | {loading ? ( 213 |
214 |
215 |
216 | ) : ( 217 | <> 218 |
219 | {currentCertificates.map((certificate) => ( 220 |
228 | {/* Certificate Image */} 229 | {certificate.courseName} 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 | 273 |
274 |
275 |
276 | ))} 277 |
278 | 279 | )} 280 | 281 | {/* Pagination Controls */} 282 |
283 | 294 | 295 | Page {currentCerticatePage} of {totalPages} 296 | 297 | 308 |
309 | 310 | {/* Add Certificate Modal */} 311 | {isModalOpen && ( 312 |
315 | {!isAuthorized ? ( 316 |
322 | 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 | 342 | 348 |
349 | ) : ( 350 |
355 | {/* Modal Title */} 356 |

361 | Add Certificate 362 |

363 | 364 | {/* Image URL Input */} 365 |
366 | 373 | 382 | setNewCertificate({ 383 | ...newCertificate, 384 | imageUrl: e.target.value, 385 | }) 386 | } 387 | autoFocus 388 | /> 389 |
390 | 391 | {/* Course Name Input */} 392 |
393 | 400 | 409 | setNewCertificate({ 410 | ...newCertificate, 411 | courseName: e.target.value, 412 | }) 413 | } 414 | /> 415 |
416 | 417 | {/* Action Buttons */} 418 |
419 | 429 | 439 |
440 |
441 | )} 442 |
443 | )} 444 |
445 | ); 446 | }; 447 | 448 | export default Certificates; 449 | --------------------------------------------------------------------------------