├── src ├── index.css ├── assets │ ├── logo.png │ └── react.svg ├── styles │ ├── Footer.module.css │ ├── Auth.module.css │ ├── RoadmapDetailsPage.module.css │ ├── ChatBot.module.css │ ├── Home.module.css │ └── Profile.module.css ├── components │ ├── Footer.jsx │ ├── ImageQuote.jsx │ ├── RoadmapItem.jsx │ ├── Navbar.jsx │ ├── TrendingFields.jsx │ ├── Roadmap.jsx │ ├── Steps.jsx │ ├── EditProfile.jsx │ ├── SignIn.jsx │ ├── SignUp.jsx │ └── ProfileCard.jsx ├── main.jsx ├── App.css ├── pages │ ├── RoadmapPage.jsx │ ├── AuthPage.jsx │ ├── HomePage.jsx │ ├── ProfilePage.jsx │ ├── RoadmapDetailsPage.jsx │ └── ChatbotPage.jsx ├── services │ ├── groq.js │ ├── chat.js │ ├── quotes.js │ ├── AuthApi.js │ └── RoadmapAPI.js └── App.jsx ├── .gitignore ├── index.html ├── vite.config.js ├── eslint.config.js ├── package.json ├── public └── vite.svg └── README.md /src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NadaFeteiha/frontend/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /src/styles/Footer.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: #223a4d; 3 | color: #95a9b3; 4 | padding: 1.5rem 0; 5 | text-align: center; 6 | } 7 | 8 | hr { 9 | width: 100%; 10 | border: 5px solid rgba(255, 255, 255, 0.1); 11 | } -------------------------------------------------------------------------------- /src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Footer.module.css"; 2 | 3 | export default function Footer() { 4 | return ( 5 | 11 | ); 12 | } -------------------------------------------------------------------------------- /src/components/ImageQuote.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/Home.module.css'; 2 | 3 | export default function QuoteImage({ imgs }) { 4 | 5 | return ( 6 |
7 | quote 8 | quote 9 |
10 | ); 11 | 12 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .env 26 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.jsx' 5 | import { BrowserRouter } from "react-router-dom"; 6 | 7 | createRoot(document.getElementById('root')).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/RoadmapItem.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/Home.module.css'; 2 | 3 | export default function RoadmapItem({ roadmap, roadmapClick }) { 4 | return ( 5 |
6 |

{roadmap.title}

7 |
8 |

{roadmap.description}

9 |
10 |
11 | ); 12 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import tailwindcss from '@tailwindcss/vite' 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | tailwindcss() 10 | ], 11 | server: { 12 | proxy: { 13 | "/api": { 14 | // target: "http://localhost:4000", 15 | target: "http://backend-moys.onrender.com", 16 | changeOrigin: true, 17 | }, 18 | }, 19 | }, 20 | }) 21 | 22 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --primary-color: #4885ad; 5 | --secondary-color: #333; 6 | --danger-color: #ff0000; 7 | --success-color: #4885ad; 8 | --warning-color: #ffcc00; 9 | --main-bg-color: #061621; 10 | } 11 | 12 | * { 13 | box-sizing: border-box; 14 | } 15 | 16 | html, 17 | body { 18 | padding: 0; 19 | margin: 0; 20 | font-family: 'Roboto', sans-serif; 21 | background-color: var(--main-bg-color); 22 | color: #4885ad; 23 | } 24 | 25 | h1 { 26 | @apply text-xl font-bold mb-2; 27 | } -------------------------------------------------------------------------------- /src/pages/RoadmapPage.jsx: -------------------------------------------------------------------------------- 1 | import RoadmapApi from "../services/RoadmapAPI"; 2 | import { useState } from "react"; 3 | 4 | //TODO: Need another page for discofer roadmaps and resources 5 | 6 | export default function RoadmapPage() { 7 | 8 | const [loading, setLoading] = useState(false); 9 | const [error, setError] = useState(null); 10 | 11 | //handel status 12 | if (loading) return

Loading...

; 13 | if (error) return

Error: {error.message}

; 14 | 15 | return ( 16 | <> 17 |

Roadmaps Search

18 | 19 | ); 20 | }; 21 | 22 | -------------------------------------------------------------------------------- /src/services/groq.js: -------------------------------------------------------------------------------- 1 | import Groq from 'groq-sdk'; 2 | 3 | // Creates a Groq sdk instance 4 | const groq = new Groq({ 5 | apiKey: import.meta.env.VITE_GROQ_API_KEY, 6 | dangerouslyAllowBrowser: true 7 | }); 8 | 9 | /** 10 | * Get Chat Completion 11 | * @description sends arrays of message to Groq API 12 | * @param {*} messages 13 | * @returns 14 | */ 15 | export async function getChatCompletion(messages) { 16 | try { 17 | const completion = await groq.chat.completions.create({ 18 | messages: messages, 19 | model: "llama-3.3-70b-versatile" 20 | }); 21 | 22 | if (!completion) { 23 | throw new Error("Error generating chat completion."); 24 | } 25 | 26 | return completion; 27 | } catch (e) { 28 | console.error(e.message) 29 | } 30 | } -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom" 2 | import styles from "../styles/Home.module.css"; 3 | import logo from "../assets/logo.png"; 4 | 5 | function Navbar() { 6 | return ( 7 | 25 | ) 26 | } 27 | 28 | export default Navbar; 29 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | 6 | export default [ 7 | { ignores: ['dist'] }, 8 | { 9 | files: ['**/*.{js,jsx}'], 10 | languageOptions: { 11 | ecmaVersion: 2020, 12 | globals: globals.browser, 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | ecmaFeatures: { jsx: true }, 16 | sourceType: 'module', 17 | }, 18 | }, 19 | plugins: { 20 | 'react-hooks': reactHooks, 21 | 'react-refresh': reactRefresh, 22 | }, 23 | rules: { 24 | ...js.configs.recommended.rules, 25 | ...reactHooks.configs.recommended.rules, 26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], 27 | 'react-refresh/only-export-components': [ 28 | 'warn', 29 | { allowConstantExport: true }, 30 | ], 31 | }, 32 | }, 33 | ] 34 | -------------------------------------------------------------------------------- /src/services/chat.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const API_URL = import.meta.env.VITE_BASE_URL; 4 | axios.defaults.baseURL = API_URL; 5 | 6 | // Create a new chat 7 | export async function createChat(messages, title) { 8 | const res = await axios.post("/api/chat", { messages, title }); 9 | return res.data; 10 | } 11 | 12 | // Get all chats 13 | export async function getChats() { 14 | const res = await axios.get("/api/chat/"); 15 | return res.data; 16 | } 17 | 18 | // Get a specific chat 19 | export async function getChat(chatId) { 20 | const res = await axios.get(`/api/chat/${chatId}`); 21 | return res.data; 22 | } 23 | 24 | // Update a chat 25 | export async function updateChat(chatId, messages, title) { 26 | const res = await axios.patch(`/api/chat/${chatId}`, { 27 | messages, 28 | title, 29 | }); 30 | return res.data; 31 | } 32 | 33 | // Delete a chat 34 | export async function deleteChat(chatId) { 35 | await axios.delete(`/api/chat/${chatId}`); 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf .cache" 12 | 13 | }, 14 | "dependencies": { 15 | "@tailwindcss/vite": "^4.0.15", 16 | "axios": "^1.8.4", 17 | "groq-sdk": "^0.17.0", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "react-icons": "^5.5.0", 21 | "react-router-dom": "^7.4.0" 22 | }, 23 | "devDependencies": { 24 | "@eslint/js": "^9.21.0", 25 | "@types/react": "^19.0.10", 26 | "@types/react-dom": "^19.0.4", 27 | "@vitejs/plugin-react": "^4.3.4", 28 | "autoprefixer": "^10.4.21", 29 | "eslint": "^9.21.0", 30 | "eslint-plugin-react-hooks": "^5.1.0", 31 | "eslint-plugin-react-refresh": "^0.4.19", 32 | "globals": "^15.15.0", 33 | "postcss": "^8.5.3", 34 | "tailwindcss": "^4.0.15", 35 | "vite": "^6.2.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/TrendingFields.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/Home.module.css'; 2 | 3 | function TrendingFields({ chatBotClick, trendTopics }) { 4 | return ( 5 |
6 |

The most common study fields or languages for this year...

7 |
8 | { 9 | trendTopics.map(topic => ( 10 | 11 | )) 12 | } 13 |
14 |
15 | 21 |
22 | ); 23 | } 24 | 25 | 26 | function TopicItem({ topic }) { 27 | return ( 28 |
29 |

{topic.title}

30 |
31 | ); 32 | } 33 | 34 | export default TrendingFields; 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/services/quotes.js: -------------------------------------------------------------------------------- 1 | 2 | const BASE_URL = "https://api.unsplash.com/"; 3 | const API_KEY = import.meta.env.VITE_UNSPLASH_API_KEY; 4 | /** 5 | * @param {string} orientation 6 | * @param {string} quote 7 | * @param {number} per_page 8 | * @description Fetch a quote image from Unsplash API 9 | */ 10 | export async function getQuoteImgs(orientation = "landscape", quote = "study quotes", per_page = 10) { 11 | try { 12 | const response = await fetch(`${BASE_URL}search/photos?query=${quote}&orientation=${orientation}&per_page=${per_page}&page=1&client_id=${API_KEY}`); 13 | if (!response.ok) { 14 | throw new Error("Failed to fetch quote"); 15 | } 16 | 17 | const data = await response.json(); 18 | const images = data.results 19 | .sort(() => 0.5 - Math.random()) 20 | .slice(0, 2) 21 | .map(result => result.urls.regular); 22 | 23 | return images; 24 | } catch (error) { 25 | console.error(error); 26 | } 27 | } 28 | 29 | function getRandomInt(max, min = 0) { 30 | return Math.floor(Math.random() * (max - min) + min); 31 | } 32 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import { Routes, Route } from 'react-router-dom' 3 | import Navbar from './components/Navbar' 4 | import Home from './pages/HomePage' 5 | import Profile from './pages/ProfilePage' 6 | import AuthPage from './pages/AuthPage' 7 | import ChatBot from './pages/ChatbotPage' 8 | import Roadmap from './pages/RoadmapPage' 9 | import RoadmapDetails from './pages/RoadmapDetailsPage' 10 | import { useState } from 'react' 11 | 12 | function App() { 13 | 14 | const [isChatbotOpen, setIsChatbotOpen] = useState(false) 15 | 16 | return ( 17 |
18 | { 19 | !isChatbotOpen && 20 | } 21 | 22 | 23 | } /> 24 | } /> 25 | } /> 26 | } /> 27 | } /> 28 | } /> 29 | 30 | 31 |
32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /src/components/Roadmap.jsx: -------------------------------------------------------------------------------- 1 | import styles from '../styles/Home.module.css'; 2 | import RoadmapItem from './RoadmapItem'; 3 | 4 | function Roadmap({ roadmaps, allRoadmapClick, roadmapClick }) { 5 | return ( 6 |
7 | 8 |
9 |

Roadmaps

10 |
11 | 12 |
13 | {roadmaps.map((roadmap) => ( 14 | roadmapClick(roadmap.id)} 18 | /> 19 | ))} 20 | 21 |
22 | 23 |
24 | ); 25 | } 26 | 27 | function MoreRoadmapItem({ roadmapClick }) { 28 | return ( 29 |
30 |

more

31 |
32 | ); 33 | } 34 | 35 | export default Roadmap; -------------------------------------------------------------------------------- /src/pages/AuthPage.jsx: -------------------------------------------------------------------------------- 1 | import AuthAPI from "../services/AuthApi"; 2 | import SignIn from "../components/SignIn"; 3 | import SignUp from "../components/SignUp"; 4 | import styles from "../styles/Auth.module.css"; 5 | import { useEffect, useState } from "react"; 6 | 7 | //TODO: Check if user is logged in or not by token 8 | //TODO: If user is logged in, redirect to Profile Page 9 | //Todo: implement login , forgot password, reset password, register 10 | //TODO: change focus when clicked on signup in signin component and vice versa 11 | 12 | function AuthPage() { 13 | 14 | const [userId, setUserId] = useState(""); 15 | 16 | useEffect(() => { 17 | if (userId) { 18 | localStorage.setItem("userID", userId); 19 | } 20 | }, [userId]); 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 32 |
33 | ); 34 | }; 35 | 36 | 37 | export default AuthPage; -------------------------------------------------------------------------------- /src/styles/Auth.module.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | /********************* Auth page *****************************/ 4 | .authContainer { 5 | @apply flex justify-around items-start h-screen align-top mt-32; 6 | } 7 | 8 | .dividerContainer { 9 | @apply flex h-[90%] mt-5; 10 | } 11 | 12 | .divider { 13 | @apply w-px h-1/2 bg-gray-300 shadow-lg; 14 | } 15 | 16 | /***************** Sign-in and register *************/ 17 | .signContainer { 18 | @apply flex flex-col w-[40%] min-h-[400px] items-center gap-4 bg-[#ffffff] p-8 rounded-md shadow-lg; 19 | } 20 | 21 | .form { 22 | @apply flex flex-col w-[100%] items-start gap-4; 23 | } 24 | 25 | .inputGroup { 26 | @apply flex flex-col text-left w-[100%]; 27 | } 28 | 29 | .inputGroup label { 30 | @apply font-medium mb-1; 31 | } 32 | 33 | .inputGroup input { 34 | @apply p-2 border border-gray-300 rounded-md text-sm; 35 | } 36 | 37 | .inputGroup input:focus { 38 | @apply focus:outline-none focus:ring-2 focus:ring-[#041323] focus:border-transparent; 39 | } 40 | 41 | .forgotPassword { 42 | @apply text-right text-sm text-gray-500 mt-1 hover:underline; 43 | } 44 | 45 | .btnContainer { 46 | @apply flex justify-center w-[100%]; 47 | } 48 | 49 | .signInButton { 50 | @apply bg-black text-white py-2 px-4 rounded-md text-sm hover:bg-[#4885ad] 51 | } 52 | 53 | .error { 54 | @apply text-red-500 text-sm mb-2; 55 | } -------------------------------------------------------------------------------- /src/components/Steps.jsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles/RoadmapDetailsPage.module.css"; 2 | 3 | 4 | export default function Steps({ steps }) { 5 | 6 | return ( 7 |
8 |
9 |

Steps

10 |
    11 | {steps.map(step => ( 12 |
  1. 13 |
    14 | Step {step.order} 15 |

    {step.title}

    16 |
    17 |
    18 | Topic: 19 | {step.topic.title} 20 |

    {step.topic.description}

    21 | {step.topic.type} 22 |
    23 |
  2. 24 | ))} 25 |
26 |
27 |
28 | ) 29 | }; 30 | 31 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Roadmap Learning Platform 2 | 3 | This project is a web-based platform designed to help users achieve their learning goals through structured roadmaps. Users can explore various roadmaps, track their progress, and interact with AI-powered chatbots for guidance. 4 | 5 | ## Features 6 | 7 | - **Roadmaps**: Explore curated learning paths with steps and topics to achieve specific goals. 8 | - **Progress Tracking**: Track your progress through roadmaps and mark steps as completed. 9 | - **User Profiles**: Manage your profile, including updating your name and profile picture. 10 | - **AI Chatbot**: Interact with an AI chatbot for personalized learning assistance. 11 | - **Trending Topics**: Discover the most popular study fields and topics. 12 | - **Resource Management**: Access resources like articles, videos, and tutorials for each topic. 13 | 14 | ## Tech Stack 15 | 16 | - **React**: A JavaScript library for building user interfaces. 17 | - **Vite**: A fast build tool for modern web projects. 18 | - **CSS Modules**: Scoped and modular CSS for styling components. 19 | - **React Router**: For client-side routing. 20 | - **Axios**: For making HTTP requests to the backend. 21 | - **React Icons**: For adding icons to the UI. 22 | 23 | ## Installation 24 | 25 | ### Steps 26 | 27 | 1. Clone the repository: 28 | 29 | ```bash 30 | git clone 31 | cd Capstone 32 | cd frontend 33 | ``` 34 | 35 | 2. Install dependencies: 36 | 37 | ```bash 38 | npm i 39 | ``` 40 | 41 | 3. Set up environment variables: 42 | 43 | ``` 44 | VITE_UNSPLASH_API_KEY= 45 | VITE_GROQ_API_KEY= 46 | ``` 47 | 48 | 4. Start the frontend development server: 49 | ```bash 50 | npm run dev 51 | ``` 52 | 53 | ## Folder Structure 54 | 55 | - **src/components**: Reusable React components. 56 | - **src/pages**: Page-level components for routing. 57 | - **src/services**: API service files for interacting with the backend. 58 | - **src/styles**: CSS modules for styling. 59 | -------------------------------------------------------------------------------- /src/components/EditProfile.jsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Profile.module.css"; 2 | import { useState } from "react"; 3 | 4 | export default function EditProfile({ user, updateUserInfo, cancel }) { 5 | const [name, setName] = useState(user?.name || ""); 6 | const [profilePicture, setProfilePicture] = useState(user?.profilePicture || ""); 7 | 8 | const handleSubmit = (event) => { 9 | event.preventDefault(); 10 | const updatedUser = { name, profilePicture }; 11 | updateUserInfo(updatedUser); 12 | }; 13 | 14 | return ( 15 |
16 |

Edit Profile

17 |
18 |
19 | Profile Preview 24 | setProfilePicture(e.target.value)} 29 | /> 30 |
31 | 32 |
33 | 34 | setName(e.target.value)} 38 | required 39 | /> 40 |
41 | 42 |
43 | 44 | 45 | 46 |
47 |
48 |
49 | ); 50 | }; -------------------------------------------------------------------------------- /src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import RoadmapApi from '../services/RoadmapAPI.js'; 4 | import TrendingFields from '../components/TrendingFields.jsx'; 5 | import Roadmap from '../components/Roadmap.jsx'; 6 | import { getQuoteImgs as QuoteImg } from '../services/quotes.js'; 7 | import QuoteImage from '../components/ImageQuote.jsx'; 8 | import Footer from '../components/Footer.jsx'; 9 | 10 | function HomePage({ setIsChatbotOpen }) { 11 | const navigate = useNavigate(); 12 | const [roadmaps, setRoadmaps] = useState([]); 13 | const [trendTopics, setTrendTopics] = useState([]); 14 | const [imgs, setImgs] = useState([]); 15 | 16 | useEffect(() => { 17 | RoadmapApi.getAllRoadmap() 18 | .then(response => { 19 | console.log(response); 20 | setRoadmaps(response); 21 | }) 22 | .catch(error => { 23 | console.log(error); 24 | }); 25 | 26 | //get triend topics 27 | RoadmapApi.mostPopularTopics() 28 | .then(response => { 29 | console.log("popular topics"); 30 | console.log(response); 31 | setTrendTopics(response); 32 | }) 33 | .catch(error => { 34 | console.log(error); 35 | }); 36 | }, []); 37 | 38 | // call the QuoteImg function to get a random quote image 39 | useEffect(() => { 40 | QuoteImg().then(response => { 41 | setImgs(response); 42 | }).catch(error => { 43 | console.log(error); 44 | }); 45 | }, []); 46 | 47 | return ( 48 |
49 | { 50 | setIsChatbotOpen(true); 51 | navigate('/chatbot') 52 | } 53 | } trendTopics={trendTopics} /> 54 | navigate('/roadmap')} 57 | roadmapClick={(roadmapId) => navigate(`/roadmap/${roadmapId}`)} 58 | /> 59 | 60 |
61 |
62 | ); 63 | } 64 | 65 | export default HomePage; -------------------------------------------------------------------------------- /src/services/AuthApi.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const API_URL = import.meta.env.VITE_BASE_URL; 4 | axios.defaults.baseURL = API_URL; 5 | axios.defaults.headers.post['Content-Type'] = 'application/json'; 6 | 7 | //TODO: forgot password, reset password 8 | 9 | const login = async (email, password) => { 10 | const response = await axios.post('/api/auth/login', { 11 | email: email, password: password 12 | }); 13 | 14 | if (response.data.status === false) { 15 | console.log(response.data); 16 | throw new Error(response.data.message); 17 | } 18 | 19 | console.log(response.data); 20 | return response.data; 21 | }; 22 | 23 | const register = async (userName, Name, email, password) => { 24 | const response = await axios.post('/api/auth/register', 25 | { email: email, userName: userName, name: Name, password: password }); 26 | 27 | if (response.data.status == false) { 28 | console.log(response.data); 29 | throw new Error(response.message); 30 | } 31 | 32 | console.log(response.data); 33 | return response.data; 34 | }; 35 | 36 | const myProfile = async (userId) => { 37 | const response = await axios.get(`/api/user/profile/${userId}`); 38 | 39 | if (response.data.status === false) { 40 | console.log(response.data); 41 | throw new Error(response.data.message); 42 | } 43 | return response.data; 44 | }; 45 | 46 | const deleteAccount = async (userId) => { 47 | const response = await axios.delete(`/api/user/${userId}`); 48 | if (response.data.status === false) { 49 | console.log(response.data); 50 | throw new Error(response.data.message); 51 | } 52 | return response.data; 53 | } 54 | 55 | const updateProfile = async (userId, updatedUser) => { 56 | console.log(`Updating user profile for user: ${userId}`); 57 | console.log(updatedUser); 58 | const response = await axios.patch(`/api/user/profile/${userId}`, 59 | { 60 | name: updatedUser.name, 61 | profilePicture: updatedUser.profilePicture 62 | } 63 | ); 64 | 65 | console.log(response.data); 66 | 67 | if (response.data.status === false) { 68 | console.log(response.data); 69 | throw new Error(response.data.message); 70 | } 71 | return response.data; 72 | } 73 | 74 | const AuthAPI = { 75 | login, 76 | register, 77 | myProfile, 78 | deleteAccount, 79 | updateProfile 80 | }; 81 | 82 | export default AuthAPI; -------------------------------------------------------------------------------- /src/services/RoadmapAPI.js: -------------------------------------------------------------------------------- 1 | 2 | import axios from 'axios'; 3 | 4 | const API_URL = import.meta.env.VITE_BASE_URL; 5 | axios.defaults.baseURL = API_URL; 6 | 7 | axios.defaults.headers.post['Content-Type'] = 'application/json'; 8 | 9 | const getAllRoadmap = async () => { 10 | const response = await axios.get(`/api/roadmap`); 11 | 12 | if (response.data.status === false) { 13 | console.log(response.data); 14 | throw new Error(response.data.message); 15 | } 16 | return response.data.data; 17 | }; 18 | 19 | 20 | const getRoadmap = async (roadmapId) => { 21 | const response = await axios.get(`/api/roadmap/${roadmapId}`); 22 | 23 | if (response.data.status === false) { 24 | console.log(response.data); 25 | throw new Error(response.data.message); 26 | } 27 | 28 | return response.data.data; 29 | }; 30 | 31 | 32 | const mostPopularTopics = async () => { 33 | const response = await axios.get(`/api/topic/popular`); 34 | 35 | if (response.data.status === false) { 36 | console.log(response.data); 37 | throw new Error(response.data.message); 38 | } 39 | return response.data.data; 40 | } 41 | 42 | const setRoadmapToUser = async (roadmapId, userId) => { 43 | const response = await axios.post(`/api/user/${userId}/roadmap/${roadmapId}`); 44 | 45 | if (response.data.status === false) { 46 | console.log(response.data); 47 | throw new Error(response.data.message); 48 | } 49 | console.log("set roadmap to user"); 50 | console.log(response); 51 | return response.data.data; 52 | } 53 | 54 | const isUserInRoadmap = async (roadmapId, userId) => { 55 | const response = await axios.get(`/api/user/${userId}/roadmap/${roadmapId}`); 56 | 57 | if (response.data.status === false) { 58 | console.log(response.data); 59 | throw new Error(response.data.message); 60 | } 61 | return response.data.data.inRoadmap; 62 | } 63 | 64 | const deleteRoadmapFromUser = async (roadmapId, userId) => { 65 | const response = await axios.delete(`/api/user/${userId}/roadmap/${roadmapId}`); 66 | 67 | if (response.data.status === false) { 68 | console.log(response.data); 69 | throw new Error(response.data.message); 70 | } 71 | return response.data.data.success; 72 | } 73 | 74 | const RoadmapApi = { 75 | getAllRoadmap, 76 | getRoadmap, 77 | mostPopularTopics, 78 | setRoadmapToUser, 79 | isUserInRoadmap, 80 | deleteRoadmapFromUser 81 | }; 82 | 83 | export default RoadmapApi; -------------------------------------------------------------------------------- /src/pages/ProfilePage.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import AuthAPI from "../services/AuthApi"; 3 | import ProfileCard from "../components/ProfileCard"; 4 | import EditProfile from "../components/EditProfile"; 5 | 6 | function ProfilePage() { 7 | const [user, setUser] = useState(null); 8 | const [isEditing, setIsEditing] = useState(false); 9 | const [error, setError] = useState(null); 10 | const [loading, setLoading] = useState(true); 11 | 12 | useEffect(() => { 13 | const userId = localStorage.getItem("userID"); 14 | setError(null); 15 | 16 | // if not logged in, redirect to login page 17 | if (!userId) { 18 | window.location.href = "/auth"; 19 | } 20 | 21 | // if logged in, get user profile 22 | setLoading(true); 23 | AuthAPI.myProfile(userId) 24 | .then((res) => { 25 | console.log(res.data); 26 | setUser(res.data); 27 | }) 28 | .catch((err) => { 29 | console.log(err); 30 | setError(err.message); 31 | }).finally(() => { 32 | setLoading(false); 33 | }); 34 | }, []); 35 | 36 | const handleEditProfile = () => { 37 | setIsEditing(true); 38 | } 39 | 40 | const updateUserInfo = (updatedUser) => { 41 | // reset 42 | setError(null); 43 | setLoading(true); 44 | 45 | AuthAPI.updateProfile(user.id, updatedUser) 46 | .then((res) => { 47 | // just reset the user details not roadmaps 48 | console.log("updated user"); 49 | console.log(res.data); 50 | const newUser = { 51 | ...user, 52 | name: res.data.name, 53 | email: res.data.email, 54 | userName: res.data.userName, 55 | role: res.data.role, 56 | profilePicture: res.data.profilePicture, 57 | } 58 | 59 | setUser(newUser); 60 | setIsEditing(false); 61 | }) 62 | .catch((err) => { 63 | console.log(err); 64 | setError(err.message); 65 | }).finally(() => { 66 | setLoading(false); 67 | }); 68 | } 69 | 70 | if (loading) return

Loading...

; 71 | if (error) return

Error: {error}

; 72 | if (!user) return

User not found.

; 73 | 74 | return ( 75 | !isEditing ? 76 | 77 | : 78 | setIsEditing(false)} /> 79 | ); 80 | }; 81 | 82 | export default ProfilePage; -------------------------------------------------------------------------------- /src/components/SignIn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from "../styles/Auth.module.css"; 3 | import { useState } from "react"; 4 | import AuthApi from "../services/AuthApi"; 5 | 6 | const SignIn = ({ setUserId }) => { 7 | const [email, setEmail] = useState(""); 8 | const [password, setPassword] = useState(""); 9 | const [loading, setLoading] = useState(false); 10 | const [error, setError] = useState(""); 11 | 12 | const handleSubmit = async (e) => { 13 | e.preventDefault(); 14 | setError("");// => RESET ERROR 15 | setLoading(true); 16 | try { 17 | const response = await AuthApi.login(email, password) 18 | // if success, redirect to home page 19 | console.log(" heere"); 20 | console.log(response); 21 | if (response.status == true) { 22 | setUserId(response.data.id); 23 | window.location.href = "/"; 24 | } 25 | } catch (error) { 26 | console.log("Error heere"); 27 | console.log(error.message); 28 | setError(error.message); 29 | } finally { 30 | setLoading(false); 31 | } 32 | }; 33 | 34 | return ( 35 |
36 |

Welcome back!

37 | 38 |
39 |
40 | 41 | setEmail(e.target.value)} 46 | required 47 | /> 48 |
49 | 50 |
51 | 52 | setPassword(e.target.value)} 57 | required 58 | /> 59 | 60 | Forgot Password? 61 | 62 |
63 | 64 | {error &&

{error}

} 65 | 66 |
67 | 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | export default SignIn; -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/RoadmapDetailsPage.jsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom" 2 | import RoadmapApi from "../services/RoadmapAPI"; 3 | import { useEffect } from "react"; 4 | import { useState } from "react"; 5 | import styles from "../styles/RoadmapDetailsPage.module.css"; 6 | import Steps from "../components/Steps"; 7 | 8 | //TODO: check if user alread started this roadmap 9 | //TODO: if yes display the last step completed 10 | //TODO: if no show start now button 11 | //TODO: let user mark step if done or not 12 | 13 | export default function RoadmapDetailsPage() { 14 | const params = useParams(); 15 | const roadmapId = params.roadmapId; 16 | const [roadmap, setRoadmap] = useState(null); 17 | const [steps, setSteps] = useState([]); 18 | const [inThisAlready, setInThisAlready] = useState(null); 19 | const [userId, setUserId] = useState(null); 20 | 21 | const [loading, setLoading] = useState(false); 22 | const [error, setError] = useState(null); 23 | 24 | useEffect(() => { 25 | setLoading(true); 26 | setError(null); 27 | 28 | RoadmapApi.getRoadmap(roadmapId) 29 | .then(data => { 30 | if (data?.roadmap) { 31 | setRoadmap(data.roadmap); 32 | setSteps(data.steps || []); 33 | } 34 | }) 35 | .catch(error => { 36 | setError(error); 37 | }).then(() => { 38 | setLoading(false); 39 | }); 40 | 41 | const userId = localStorage.getItem("userID"); 42 | setUserId(userId); 43 | if (!userId) { 44 | setInThisAlready(false); 45 | } 46 | 47 | // show the button if not in this roadmap 48 | RoadmapApi.isUserInRoadmap(roadmapId, userId).then(data => { 49 | console.log("=========== is in roadmap ==========="); 50 | console.log(data); 51 | setInThisAlready(data); 52 | }).catch(error => { 53 | console.error(error); 54 | }); 55 | }, [roadmapId]); 56 | 57 | const handleStart = () => { 58 | if (!userId) { 59 | window.location.href = "/auth"; 60 | } 61 | 62 | RoadmapApi.setRoadmapToUser(roadmapId, userId) 63 | .then(data => { 64 | if (data?.success) { 65 | setInThisAlready(true); 66 | } 67 | }).catch(error => { 68 | console.error(error); 69 | }); 70 | }; 71 | 72 | const handleDelete = () => { 73 | RoadmapApi.deleteRoadmapFromUser(roadmapId, userId) 74 | .then(isSuccess => { 75 | console.log("=========== delete roadmap ==========="); 76 | console.log(isSuccess); 77 | setInThisAlready(!isSuccess); 78 | }).catch(error => { 79 | console.error(error); 80 | }); 81 | }; 82 | 83 | //handel status 84 | if (loading) return

Loading...

; 85 | if (error) return

Error: {error.message}

; 86 | if (!roadmap) return

No roadmap found.

; 87 | 88 | return ( 89 |
90 |
91 |
92 |

{roadmap.title}

93 |

{roadmap.description}

94 |
95 | {!inThisAlready && } 96 | {inThisAlready && userId && } 97 | {/* TODO progress bar for the user show his progress in case he already in the roadmap. */} 98 |
99 | 100 |
101 |

Total Steps: {roadmap.totalSteps}

102 |

Total Topics: {roadmap.totalTopics}

103 |

Last Updated: {new Date(roadmap.lastUpdated).toLocaleDateString()}

104 |
105 | 106 | 107 |
108 | ); 109 | }; -------------------------------------------------------------------------------- /src/styles/RoadmapDetailsPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | } 7 | 8 | .headerContainer { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | margin-bottom: 20px; 13 | padding: 20px; 14 | } 15 | 16 | h2 { 17 | font-size: 28px; 18 | color: #20c060; 19 | margin-bottom: 10px; 20 | } 21 | 22 | p { 23 | font-size: 18px; 24 | color: #b6c0c5; 25 | margin-bottom: 20px; 26 | } 27 | 28 | button { 29 | display: inline-block; 30 | padding: 10px 20px; 31 | font-size: 16px; 32 | color: white; 33 | background-color: #28a745; 34 | border: none; 35 | border-radius: 5px; 36 | cursor: pointer; 37 | } 38 | 39 | button:hover { 40 | background-color: #218838; 41 | } 42 | 43 | .roadmapInfo { 44 | margin: 0 20px 0 20px; 45 | padding: 15px; 46 | background: #e3eaf5; 47 | border-left: 5px solid #4a90e2; 48 | border-radius: 5px; 49 | } 50 | 51 | .roadmapInfo p { 52 | margin: 5px 0; 53 | font-size: 16px; 54 | color: #333; 55 | } 56 | 57 | /********** Step component ****************/ 58 | .mainStepContainer { 59 | width: 100%; 60 | height: 100%; 61 | } 62 | 63 | .stepsContainer { 64 | width: 100%; 65 | display: flex; 66 | flex-direction: column; 67 | max-width: 800px; 68 | margin: 0 auto; 69 | padding: 2rem; 70 | } 71 | 72 | .stepsTitle { 73 | font-size: 2rem; 74 | color: #b6c0c5; 75 | margin-bottom: 2rem; 76 | text-align: center; 77 | position: relative; 78 | padding-bottom: 0.5rem; 79 | } 80 | 81 | .stepsTitle::after { 82 | content: ''; 83 | position: absolute; 84 | bottom: 0; 85 | left: 50%; 86 | transform: translateX(-50%); 87 | width: 80px; 88 | height: 4px; 89 | background: linear-gradient(90deg, #3498db, #9b59b6); 90 | border-radius: 2px; 91 | } 92 | 93 | .stepsList { 94 | list-style: none; 95 | padding: 0; 96 | counter-reset: step-counter; 97 | } 98 | 99 | .stepItem { 100 | position: relative; 101 | background: #dfe5e8; 102 | padding: 1.5rem; 103 | margin-bottom: 1.5rem; 104 | border-radius: 8px; 105 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 106 | counter-increment: step-counter; 107 | } 108 | 109 | .stepItem:hover { 110 | transform: translateY(-3px); 111 | box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); 112 | } 113 | 114 | .stepItem::before { 115 | content: counter(step-counter); 116 | position: absolute; 117 | top: -15px; 118 | left: -15px; 119 | width: 40px; 120 | height: 40px; 121 | background: linear-gradient(135deg, #3498db, #9b59b6); 122 | color: white; 123 | border-radius: 50%; 124 | display: flex; 125 | align-items: center; 126 | justify-content: center; 127 | font-weight: bold; 128 | font-size: 1.2rem; 129 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 130 | } 131 | 132 | .stepHeader { 133 | margin-bottom: 1rem; 134 | display: flex; 135 | align-items: center; 136 | gap: 1rem; 137 | } 138 | 139 | .stepNumber { 140 | background: #f0f8ff; 141 | color: #2980b9; 142 | padding: 0.3rem 0.8rem; 143 | border-radius: 20px; 144 | font-weight: bold; 145 | font-size: 0.9rem; 146 | } 147 | 148 | .stepTitle { 149 | margin: 0; 150 | color: #2c3e50; 151 | font-size: 1.3rem; 152 | } 153 | 154 | .topicInfo { 155 | background: #f9f9f9; 156 | padding: 1rem; 157 | border-radius: 6px; 158 | border-left: 4px solid #9b59b6; 159 | } 160 | 161 | .topicLabel { 162 | font-weight: bold; 163 | color: #8e44ad; 164 | margin-right: 0.5rem; 165 | } 166 | 167 | .topicTitle { 168 | color: #2c3e50; 169 | font-weight: 500; 170 | } 171 | 172 | .topicDescription { 173 | color: #555; 174 | margin: 0.5rem 0; 175 | line-height: 1.5; 176 | } 177 | 178 | .topicType { 179 | display: inline-block; 180 | background: #e0e0e0; 181 | color: #555; 182 | padding: 0.2rem 0.5rem; 183 | border-radius: 4px; 184 | font-size: 0.8rem; 185 | text-transform: uppercase; 186 | } 187 | 188 | .deleteBtn { 189 | background: #e74c3c; 190 | color: white; 191 | padding: 0.5rem 1rem; 192 | border: none; 193 | border-radius: 4px; 194 | cursor: pointer; 195 | } -------------------------------------------------------------------------------- /src/components/SignUp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useState } from "react"; 3 | import styles from "../styles/Auth.module.css"; 4 | import AuthApi from "../services/AuthApi"; 5 | 6 | //TODO: cheeck if usename is already taken 7 | 8 | const SignUp = ({ setUserId }) => { 9 | const [email, setEmail] = useState(""); 10 | const [userName, setUserName] = useState(""); 11 | const [name, setName] = useState(""); 12 | const [password, setPassword] = useState(""); 13 | const [confirmPassword, setConfirmPassword] = useState(""); 14 | 15 | const [loading, setLoading] = useState(false); 16 | const [error, setError] = useState(""); 17 | 18 | const handleSubmit = async (e) => { 19 | e.preventDefault(); 20 | 21 | setError("");// => RESET ERROR 22 | setLoading(true); 23 | 24 | try { 25 | const response = await AuthApi.register(userName, name, email, password); 26 | // if success, redirect to home page 27 | console.log(response); 28 | if (response.status == true) { 29 | setUserId(response.data.id); 30 | window.location.href = "/"; 31 | } 32 | } 33 | catch (error) { 34 | setError(error.message); 35 | } 36 | finally { 37 | setLoading(false); 38 | } 39 | }; 40 | 41 | 42 | return ( 43 |
44 |

Create your account

45 | 46 |
47 |
48 | 49 | setName(e.target.value)} 54 | required 55 | /> 56 |
57 | 58 |
59 | 60 | setUserName(e.target.value)} 65 | required 66 | /> 67 |
68 | 69 |
70 | 71 | setEmail(e.target.value)} 76 | required 77 | /> 78 |
79 | 80 |
81 | 82 | setPassword(e.target.value)} 87 | required 88 | /> 89 |
90 | 91 |
92 | 93 | setConfirmPassword(e.target.value)} 98 | required 99 | /> 100 |
101 | 102 | {error &&

{error}

} 103 | {password !== confirmPassword &&

Passwords do not match

} 104 | 105 |
106 | 109 |
110 |
111 |
112 | ); 113 | }; 114 | 115 | export default SignUp; -------------------------------------------------------------------------------- /src/styles/ChatBot.module.css: -------------------------------------------------------------------------------- 1 | .mainContainer { 2 | display: flex; 3 | background: #061621; 4 | color: #b6c0c5; 5 | height: 100vh; 6 | } 7 | 8 | .sidebar { 9 | width: 300px; 10 | background: rgba(255, 255, 255, 0.05); 11 | border-right: 1px solid rgba(255, 255, 255, 0.1); 12 | display: flex; 13 | flex-direction: column; 14 | padding: 1.5rem; 15 | } 16 | 17 | .sidebarHeader { 18 | display: flex; 19 | justify-content: space-between; 20 | align-items: center; 21 | margin-bottom: 1.5rem; 22 | padding-bottom: 1rem; 23 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 24 | } 25 | 26 | .title { 27 | font-size: 1.2rem; 28 | font-weight: 600; 29 | color: #3498db; 30 | margin: 0; 31 | } 32 | 33 | .iconsContainer { 34 | display: flex; 35 | gap: 1rem; 36 | } 37 | 38 | .icon { 39 | font-size: 1.2rem; 40 | color: #b6c0c5; 41 | cursor: pointer; 42 | transition: color 0.3s ease; 43 | } 44 | 45 | .newChatIcon:hover { 46 | color: #3498db; 47 | } 48 | 49 | .chatsList { 50 | flex: 1; 51 | overflow-y: auto; 52 | margin-bottom: 1rem; 53 | } 54 | 55 | .chatItem { 56 | padding: 0.8rem; 57 | margin-bottom: 0.5rem; 58 | border-radius: 6px; 59 | cursor: pointer; 60 | display: flex; 61 | justify-content: space-between; 62 | align-items: center; 63 | transition: all 0.3s ease; 64 | } 65 | 66 | .chatItem:hover { 67 | background: rgba(255, 255, 255, 0.1); 68 | } 69 | 70 | .activeChat { 71 | background: rgba(52, 152, 219, 0.2); 72 | border-left: 3px solid #3498db; 73 | } 74 | 75 | .chatTitle { 76 | white-space: nowrap; 77 | overflow: hidden; 78 | text-overflow: ellipsis; 79 | flex: 1; 80 | } 81 | 82 | .deleteButton { 83 | background: none; 84 | border: none; 85 | color: #d9534f; 86 | font-size: 1.2rem; 87 | cursor: pointer; 88 | padding: 0 0.5rem; 89 | } 90 | 91 | .deleteButton:hover { 92 | color: #c9302c; 93 | } 94 | 95 | .userInfo { 96 | display: flex; 97 | align-items: center; 98 | gap: 0.8rem; 99 | padding-top: 1rem; 100 | border-top: 1px solid rgba(255, 255, 255, 0.1); 101 | margin-top: auto; 102 | } 103 | 104 | .userIcon { 105 | font-size: 1.8rem; 106 | border: 1px solid #b6c0c5; 107 | border-radius: 50%; 108 | padding: 0.3rem; 109 | } 110 | 111 | .userName { 112 | font-size: 1rem; 113 | color: #3498db; 114 | margin: 0; 115 | } 116 | 117 | .chatContainer { 118 | flex: 1; 119 | display: flex; 120 | flex-direction: column; 121 | background: rgba(255, 255, 255, 0.03); 122 | } 123 | 124 | .chatHeader { 125 | font-size: 1.2rem; 126 | font-weight: 600; 127 | color: #3498db; 128 | padding: 1rem; 129 | background: rgba(255, 255, 255, 0.05); 130 | margin: 0; 131 | text-align: center; 132 | } 133 | 134 | .messagesContainer { 135 | flex: 1; 136 | padding: 1.5rem; 137 | overflow-y: auto; 138 | } 139 | 140 | .message { 141 | display: flex; 142 | align-items: flex-start; 143 | margin-bottom: 1.5rem; 144 | gap: 1rem; 145 | } 146 | 147 | .messageIcon { 148 | font-size: 1.5rem; 149 | margin-top: 20px; 150 | } 151 | 152 | .messageContent { 153 | background: rgba(255, 255, 255, 0.05); 154 | padding: 1rem; 155 | border-radius: 8px; 156 | border-left: 3px solid #3498db; 157 | flex: 1; 158 | } 159 | 160 | .messageContentRight { 161 | background: rgba(255, 255, 255, 0.05); 162 | padding: 1rem; 163 | border-radius: 8px; 164 | border-right: 3px solid #3498db; 165 | flex: 1; 166 | } 167 | 168 | .inputForm { 169 | display: flex; 170 | gap: 1rem; 171 | padding: 1rem; 172 | background: rgba(255, 255, 255, 0.05); 173 | margin-top: auto; 174 | } 175 | 176 | .promptInput { 177 | flex: 1; 178 | padding: 0.8rem 1rem; 179 | border: 1px solid rgba(255, 255, 255, 0.1); 180 | border-radius: 6px; 181 | background: rgba(255, 255, 255, 0.05); 182 | color: #fff; 183 | font-size: 1rem; 184 | } 185 | 186 | .promptInput:focus { 187 | outline: none; 188 | border-color: #3498db; 189 | } 190 | 191 | .submitButton { 192 | padding: 0 1.5rem; 193 | background: #3498db; 194 | color: white; 195 | border: none; 196 | border-radius: 6px; 197 | cursor: pointer; 198 | font-weight: 500; 199 | transition: background 0.3s ease; 200 | } 201 | 202 | .submitButton:hover { 203 | background: #2980b9; 204 | } 205 | 206 | .disclaimer { 207 | text-align: center; 208 | font-size: 0.8rem; 209 | color: rgba(182, 192, 197, 0.6); 210 | padding: 0.5rem; 211 | background: rgba(255, 255, 255, 0.03); 212 | } -------------------------------------------------------------------------------- /src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | padding: 15px 20px; 3 | display: flex; 4 | align-items: center; 5 | border-bottom: 2px solid #64849c; 6 | } 7 | 8 | .logo { 9 | width: 60px; 10 | height: auto; 11 | } 12 | 13 | .navbar ul { 14 | list-style: none; 15 | display: flex; 16 | gap: 20px; 17 | margin: 0; 18 | padding: 0; 19 | width: 100%; 20 | align-items: center; 21 | } 22 | 23 | .navbar li { 24 | display: inline; 25 | } 26 | 27 | .navbar a { 28 | text-decoration: none; 29 | color: #5eade1; 30 | font-weight: bold; 31 | font-size: 18px; 32 | } 33 | 34 | .navbar a:hover { 35 | color: #b4d3e8; 36 | } 37 | 38 | .auth { 39 | margin-left: auto; 40 | } 41 | 42 | /********** Trending fields *************/ 43 | .container { 44 | display: flex; 45 | flex-direction: column; 46 | align-items: center; 47 | background-color: #516773; 48 | color: #041323; 49 | height: 50%; 50 | justify-content: center; 51 | padding: 20px; 52 | } 53 | 54 | .subtitle { 55 | font-size: 2rem; 56 | color: #f3fbff; 57 | margin-bottom: 2rem; 58 | text-align: center; 59 | position: relative; 60 | padding-bottom: 0.5rem; 61 | } 62 | 63 | .subtitle:after { 64 | content: ''; 65 | position: absolute; 66 | bottom: 0; 67 | left: 50%; 68 | transform: translateX(-50%); 69 | width: 600px; 70 | height: 4px; 71 | background: linear-gradient(90deg, #3498db, #9b59b6); 72 | border-radius: 2px; 73 | } 74 | 75 | .chatButton { 76 | background-color: white; 77 | color: black; 78 | padding: 10px 20px; 79 | border: none; 80 | cursor: pointer; 81 | font-size: 16px; 82 | border-radius: 5px; 83 | margin-bottom: 30px; 84 | } 85 | 86 | .chatButton:hover { 87 | background-color: #e0e0e0; 88 | } 89 | 90 | .heading { 91 | font-size: 24px; 92 | margin-bottom: 20px; 93 | } 94 | 95 | .trendTopicContainer { 96 | display: flex; 97 | gap: 20px; 98 | } 99 | 100 | .trendTopic { 101 | padding: 10px 20px; 102 | border-radius: 5px; 103 | background-color: #64849c; 104 | color: white; 105 | cursor: pointer; 106 | } 107 | 108 | .circle { 109 | width: 60px; 110 | height: 60px; 111 | background-color: white; 112 | border-radius: 50%; 113 | } 114 | 115 | /************* Roadmap component ****************/ 116 | .mainContainer { 117 | width: 100%; 118 | height: 100%; 119 | display: flex; 120 | flex-direction: column; 121 | padding: 2rem; 122 | } 123 | 124 | .roadmapContainer { 125 | width: 100%; 126 | display: flex; 127 | flex-direction: column; 128 | max-width: 800px; 129 | margin: 0 auto; 130 | padding: 2rem; 131 | } 132 | 133 | .header { 134 | display: flex; 135 | justify-content: center; 136 | align-items: center; 137 | margin-bottom: 2rem; 138 | } 139 | 140 | .stepsTitle { 141 | font-size: 2rem; 142 | color: #b6c0c5; 143 | margin: 0; 144 | position: relative; 145 | padding-bottom: 0.5rem; 146 | } 147 | 148 | .stepsTitle:after { 149 | content: ''; 150 | position: absolute; 151 | bottom: 0; 152 | left: 0; 153 | width: 150px; 154 | height: 4px; 155 | background: linear-gradient(90deg, #3498db, #9b59b6); 156 | border-radius: 2px; 157 | } 158 | 159 | .viewAllButton { 160 | width: 120px; 161 | padding: 0.6rem 1.2rem; 162 | background: #3498db; 163 | color: white; 164 | border: none; 165 | border-radius: 5px; 166 | font-weight: bold; 167 | cursor: pointer; 168 | transition: all 0.3s ease; 169 | } 170 | 171 | .viewAllButton:hover { 172 | background: #2980b9; 173 | transform: translateY(-2px); 174 | } 175 | 176 | /* Roadmap List Styles */ 177 | .roadmapContainer { 178 | display: flex; 179 | flex-direction: column; 180 | gap: 2rem; 181 | } 182 | 183 | .roadmapListContainer { 184 | display: flex; 185 | flex-direction: flex-start; 186 | flex-wrap: wrap; 187 | gap: 1rem; 188 | } 189 | 190 | .roadmapItem { 191 | background: #dfe5e8; 192 | padding: 1.5rem; 193 | border-radius: 8px; 194 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 195 | cursor: pointer; 196 | } 197 | 198 | .roadmapTitle { 199 | margin: 0; 200 | color: #2c3e50; 201 | font-size: 1.3rem; 202 | } 203 | 204 | .roadmapDescription { 205 | color: #555; 206 | margin: 0 0 0.5rem 0; 207 | line-height: 1.5; 208 | } 209 | 210 | /*********** Image component ***************/ 211 | /* make the imge full width */ 212 | .quoteImageContainer { 213 | display: flex; 214 | flex-direction: row; 215 | align-items: center; 216 | } 217 | 218 | .quoteImageContainer img { 219 | width: 500%; 220 | height: 300px; 221 | border-radius: 8px; 222 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 223 | } 224 | 225 | 226 | .moreRoadmap { 227 | background: #3d687d; 228 | padding: 1.5rem; 229 | border-radius: 8px; 230 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 231 | cursor: pointer; 232 | display: flex; 233 | flex-direction: column; 234 | justify-content: center; 235 | } 236 | 237 | .more { 238 | color: #f3fbff; 239 | font-size: 1.3rem; 240 | margin: 0; 241 | font-size: 1.3rem; 242 | font-weight: bolder; 243 | font-style: italic; 244 | } -------------------------------------------------------------------------------- /src/styles/Profile.module.css: -------------------------------------------------------------------------------- 1 | /*********** Edit Profile **********/ 2 | .editContainer { 3 | max-width: 400px; 4 | margin: 40px auto; 5 | padding: 20px; 6 | background: #fff; 7 | box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); 8 | border-radius: 8px; 9 | text-align: center; 10 | } 11 | 12 | h1 { 13 | font-size: 22px; 14 | margin-bottom: 20px; 15 | } 16 | 17 | .profileImgSection { 18 | display: flex; 19 | flex-direction: column; 20 | align-items: center; 21 | margin-bottom: 20px; 22 | } 23 | 24 | .profilePreview { 25 | width: 100px; 26 | height: 100px; 27 | border-radius: 50%; 28 | border: 2px solid #ddd; 29 | margin-bottom: 10px; 30 | } 31 | 32 | .inputGroup { 33 | margin-bottom: 15px; 34 | text-align: left; 35 | } 36 | 37 | .inputGroup label { 38 | display: block; 39 | font-weight: bold; 40 | margin-bottom: 5px; 41 | } 42 | 43 | .inputGroup input, 44 | .profileImgSection input { 45 | width: 100%; 46 | padding: 8px; 47 | font-size: 16px; 48 | border: 1px solid #ccc; 49 | border-radius: 5px; 50 | } 51 | 52 | .saveButton:hover { 53 | background: #0056b3; 54 | } 55 | 56 | .buttonGroup { 57 | display: flex; 58 | justify-content: center; 59 | gap: 10px; 60 | } 61 | 62 | 63 | /********* Profile card **************/ 64 | .mainContainer { 65 | width: 100%; 66 | height: 100%; 67 | } 68 | 69 | .profileContainer { 70 | width: 100%; 71 | display: flex; 72 | flex-direction: column; 73 | max-width: 800px; 74 | margin: 0 auto; 75 | padding: 2rem; 76 | } 77 | 78 | .stepsTitle { 79 | font-size: 2rem; 80 | color: #b6c0c5; 81 | margin-bottom: 2rem; 82 | text-align: center; 83 | position: relative; 84 | padding-bottom: 0.5rem; 85 | } 86 | 87 | .stepsTitle::after { 88 | content: ''; 89 | position: absolute; 90 | bottom: 0; 91 | left: 50%; 92 | transform: translateX(-50%); 93 | width: 100px; 94 | height: 4px; 95 | background: linear-gradient(90deg, #3498db, #9b59b6); 96 | border-radius: 2px; 97 | } 98 | 99 | .profileSection { 100 | display: flex; 101 | gap: 2rem; 102 | align-items: center; 103 | background: #dfe5e8; 104 | padding: 1.5rem; 105 | border-radius: 8px; 106 | margin-bottom: 2rem; 107 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 108 | } 109 | 110 | .avatarContainer { 111 | position: relative; 112 | } 113 | 114 | .profilePicture { 115 | width: 120px; 116 | height: 120px; 117 | border-radius: 50%; 118 | object-fit: cover; 119 | border: 5px solid transparent; 120 | background: 121 | linear-gradient(#dfe5e8, #dfe5e8) padding-box, 122 | linear-gradient(135deg, #3498db, #9b59b6) border-box; 123 | } 124 | 125 | .profileDetails { 126 | flex-grow: 1; 127 | display: flex; 128 | flex-direction: column; 129 | gap: 0.8rem; 130 | } 131 | 132 | .detailItem { 133 | display: flex; 134 | gap: 0.5rem; 135 | } 136 | 137 | .detailLabel { 138 | font-weight: bold; 139 | color: #2c3e50; 140 | } 141 | 142 | .detailValue { 143 | color: #2c3e50; 144 | } 145 | 146 | .role { 147 | display: inline-block; 148 | background: #e0e0e0; 149 | color: #555; 150 | padding: 0.2rem 0.5rem; 151 | border-radius: 4px; 152 | font-size: 0.8rem; 153 | text-transform: uppercase; 154 | } 155 | 156 | /* Progress/Roadmap Section */ 157 | .progressContainer { 158 | margin-bottom: 2rem; 159 | } 160 | 161 | .stepsList { 162 | list-style: none; 163 | padding: 0; 164 | counter-reset: step-counter; 165 | } 166 | 167 | .stepItem { 168 | position: relative; 169 | background: #dfe5e8; 170 | padding: 1.5rem; 171 | margin-bottom: 1.5rem; 172 | border-radius: 8px; 173 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); 174 | counter-increment: step-counter; 175 | } 176 | 177 | 178 | .stepHeader { 179 | margin-bottom: 1rem; 180 | display: flex; 181 | align-items: center; 182 | gap: 1rem; 183 | } 184 | 185 | .stepNumber { 186 | background: #f0f8ff; 187 | color: #2980b9; 188 | padding: 0.3rem 0.8rem; 189 | border-radius: 20px; 190 | font-weight: bold; 191 | font-size: 0.9rem; 192 | } 193 | 194 | .stepTitle { 195 | margin: 0; 196 | color: #2c3e50; 197 | font-size: 1.3rem; 198 | } 199 | 200 | .topicInfo { 201 | background: #f9f9f9; 202 | padding: 1rem; 203 | border-radius: 6px; 204 | border-left: 4px solid #9b59b6; 205 | } 206 | 207 | .infoRow { 208 | margin-bottom: 0.5rem; 209 | } 210 | 211 | .topicLabel { 212 | font-weight: bold; 213 | color: #8e44ad; 214 | margin-right: 0.5rem; 215 | } 216 | 217 | .topicTitle, 218 | .topicDescription, 219 | .topicType { 220 | color: #2c3e50; 221 | } 222 | 223 | .actionButtons { 224 | display: flex; 225 | justify-content: center; 226 | gap: 1rem; 227 | margin-top: 2rem; 228 | } 229 | 230 | .editButton, 231 | .logoutButton, 232 | .deleteButton { 233 | padding: 0.8rem 1.5rem; 234 | border: none; 235 | border-radius: 5px; 236 | font-weight: bold; 237 | cursor: pointer; 238 | transition: all 0.3s ease; 239 | } 240 | 241 | .editButton { 242 | background: #3498db; 243 | color: white; 244 | } 245 | 246 | .editButton:hover { 247 | background: #2980b9; 248 | } 249 | 250 | .logoutButton { 251 | background: #207180; 252 | color: white; 253 | } 254 | 255 | .logoutButton:hover { 256 | background: #1a5f6b; 257 | } 258 | 259 | .deleteButton { 260 | background: #d9534f; 261 | color: white; 262 | } 263 | 264 | .deleteButton:hover { 265 | background: #c9302c; 266 | } -------------------------------------------------------------------------------- /src/components/ProfileCard.jsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Profile.module.css"; 2 | import AuthAPI from "../services/AuthApi"; 3 | 4 | export default function ProfileCard({ user, handleEditProfile }) { 5 | 6 | const handleLogout = () => { 7 | localStorage.removeItem("userID"); 8 | window.location.href = "/auth"; 9 | } 10 | 11 | const handleDeleteAccount = () => { 12 | localStorage.removeItem("userID"); 13 | AuthAPI.deleteAccount(user.id); 14 | window.location.href = "/auth"; 15 | } 16 | 17 | const calculateCompletion = (roadmap) => { 18 | if (roadmap.totalSteps === 0) { 19 | return 0; 20 | } else { 21 | return Math.round((roadmap.completedCount / roadmap.totalSteps) * 100); 22 | } 23 | } 24 | 25 | return ( 26 |
27 |
28 |

Profile

29 | 30 |
31 |
32 | {user.name} 37 |
38 |
39 |
40 | Name: 41 | {user.name} 42 |
43 |
44 | Username: 45 | @{user.userName} 46 |
47 |
48 | Email: 49 | {user.email} 50 |
51 |
52 | Role: 53 | {user.role} 54 |
55 |
56 |
57 | 58 | {user.roadmaps && user.roadmaps.length > 0 && ( 59 |
60 |

My Roadmaps

61 |
    62 | {user.roadmaps.map((roadmap) => ( 63 |
  1. 64 |
    65 | Roadmap 66 |

    {roadmap.title}

    67 |
    68 |
    69 |
    70 | Description: 71 | {roadmap.description} 72 |
    73 | {roadmap.currentStep && ( 74 |
    75 | Current Step: 76 | 77 | {roadmap.currentStep.order}. {roadmap.currentStep.title} 78 | 79 |
    80 | )} 81 |
    82 | Progress: 83 | 84 | {calculateCompletion(roadmap)}% ({roadmap.completedCount}/{roadmap.totalSteps} steps) 85 | 86 |
    87 |
    88 | Started: 89 | 90 | {new Date(roadmap.startedAt).toLocaleDateString()} 91 | 92 |
    93 |
    94 | Last Active: 95 | 96 | {new Date(roadmap.lastActive).toLocaleDateString()} 97 | 98 |
    99 |
    100 |
  2. 101 | ))} 102 |
103 |
104 | )} 105 | 106 |
107 | 110 | 113 | 116 |
117 |
118 |
119 | ); 120 | } -------------------------------------------------------------------------------- /src/pages/ChatbotPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useEffect } from "react"; 2 | import { getChatCompletion } from "../services/groq"; 3 | import { CiUser } from "react-icons/ci"; 4 | import { LuBot } from "react-icons/lu"; 5 | import { IoIosSearch } from "react-icons/io"; 6 | import { FaRegPenToSquare } from "react-icons/fa6"; 7 | import { BsWindowSidebar } from "react-icons/bs"; 8 | import { 9 | getChats, 10 | getChat, 11 | createChat, 12 | updateChat, 13 | deleteChat, 14 | } from "../services/chat"; 15 | import styles from "../styles/ChatBot.module.css"; 16 | 17 | function ChatBot() { 18 | const promptInputRef = useRef(null); 19 | const [prompt, setPrompt] = useState(""); 20 | const [messages, setMessages] = useState([]); 21 | const [chats, setChats] = useState([]); 22 | const [currentChatId, setCurrentChatId] = useState(null); 23 | 24 | useEffect(() => { 25 | const fetchChats = async () => { 26 | try { 27 | const allChats = await getChats(); 28 | setChats(allChats); 29 | } catch (error) { 30 | console.error("Failed to fetch chats:", error); 31 | } 32 | }; 33 | fetchChats(); 34 | }, []); 35 | 36 | useEffect(() => { 37 | promptInputRef.current.focus(); 38 | }, []); 39 | 40 | const handleSubmit = async (e) => { 41 | e.preventDefault(); 42 | const newMessage = { role: "user", content: prompt }; 43 | const newMessages = [...messages, newMessage]; 44 | 45 | try { 46 | const completion = await getChatCompletion(newMessages); 47 | const assistantMessage = { 48 | role: "assistant", 49 | content: completion.choices[0].message.content, 50 | }; 51 | 52 | const updatedMessages = [...newMessages, assistantMessage]; 53 | if (currentChatId) { 54 | await updateChat(currentChatId, [newMessage, assistantMessage]); 55 | } else { 56 | const newChat = await createChat( 57 | updatedMessages, 58 | prompt.substring(0, 30) + "..." 59 | ); 60 | setCurrentChatId(newChat._id); 61 | setChats([newChat, ...chats]); 62 | } 63 | 64 | setMessages(updatedMessages); 65 | setPrompt(""); 66 | } catch (error) { 67 | console.error("Error processing chat:", error); 68 | } 69 | }; 70 | 71 | const handleChatSelect = async (chatId) => { 72 | try { 73 | const chat = await getChat(chatId); 74 | setMessages(chat.messages); 75 | setCurrentChatId(chatId); 76 | } catch (error) { 77 | console.error("Error loading chat:", error); 78 | } 79 | }; 80 | 81 | const handleNewChat = () => { 82 | setMessages([]); 83 | setCurrentChatId(null); 84 | }; 85 | 86 | const handleDeleteChat = async (chatId) => { 87 | try { 88 | await deleteChat(chatId); 89 | setChats(chats.filter((chat) => chat._id !== chatId)); 90 | if (currentChatId === chatId) { 91 | setMessages([]); 92 | setCurrentChatId(null); 93 | } 94 | } catch (error) { 95 | console.error("Error deleting chat:", error); 96 | } 97 | }; 98 | 99 | return ( 100 |
101 | {/* history.. */} 102 |
103 |
104 | 105 |

Chatbot

106 |
107 | 108 | 112 |
113 |
114 | 115 | {/* PREVIOUS CHATS */} 116 |
117 | {chats.map((chat) => ( 118 |
handleChatSelect(chat._id)} 123 | > 124 | {chat.title} 125 | 134 |
135 | ))} 136 |
137 | 138 |
139 | 140 |

User

141 |
142 |
143 | 144 | {/* conversation */} 145 |
146 |

Chat

147 |
148 | {messages.map((message, idx) => ( 149 |
156 | {message.role === "user" ? ( 157 | <> 158 |
159 | {message.content} 160 |
161 | 162 | 163 | ) : ( 164 | <> 165 | 166 |
167 | {message.content} 168 |
169 | 170 | )} 171 |
172 | ))} 173 |
174 | 175 |
176 | setPrompt(e.target.value)} 183 | required 184 | /> 185 | 188 |
189 |
190 |
191 | ); 192 | } 193 | 194 | export default ChatBot; --------------------------------------------------------------------------------