├── src ├── index.css ├── App.css ├── css │ ├── Navbar.css │ └── professorDetails.css ├── main.jsx ├── App.jsx ├── pages │ ├── Auth.jsx │ ├── HomePage.jsx │ └── ProfessorDetails.jsx ├── components │ └── NavBar.jsx └── assets │ └── react.svg ├── .env.development ├── vite.config.js ├── .gitignore ├── index.html ├── README.md ├── package.json ├── eslint.config.js └── public └── vite.svg /src/index.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_API_BASE_URL =http://localhost:5000 -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /* #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } */ -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /src/css/Navbar.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | margin-bottom: 25px; 3 | display: flex; 4 | align-items: center; 5 | justify-content: space-between; 6 | border: 2px black; 7 | padding: 10px; 8 | background-color: burlywood; 9 | 10 | 11 | } 12 | 13 | .auth-section{ 14 | display: flex; 15 | justify-content: space-around; 16 | align-items: center; 17 | } 18 | -------------------------------------------------------------------------------- /.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 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | .env.development 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App.jsx"; 4 | import "./index.css"; 5 | import { ClerkProvider } from "@clerk/clerk-react"; 6 | import { BrowserRouter as Router } from "react-router-dom"; 7 | const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY; 8 | 9 | if (!PUBLISHABLE_KEY) { 10 | throw new Error("Missing Publishable Key"); 11 | } 12 | 13 | 14 | createRoot(document.getElementById("root")).render( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /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 | }, 12 | "dependencies": { 13 | "@clerk/clerk-react": "^5.13.1", 14 | "axios": "^1.7.7", 15 | "bootstrap": "^5.3.3", 16 | "react": "^18.3.1", 17 | "react-bootstrap": "^2.10.5", 18 | "react-dom": "^18.3.1", 19 | "react-icons": "^5.3.0", 20 | "react-router-dom": "^6.27.0" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.11.1", 24 | "@types/react": "^18.3.10", 25 | "@types/react-dom": "^18.3.0", 26 | "@vitejs/plugin-react": "^4.3.2", 27 | "eslint": "^9.11.1", 28 | "eslint-plugin-react": "^7.37.0", 29 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 30 | "eslint-plugin-react-refresh": "^0.4.12", 31 | "globals": "^15.9.0", 32 | "vite": "^5.4.8" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Routes, 5 | Route, 6 | Navigate, 7 | } from "react-router-dom"; 8 | import HomePage from "./pages/HomePage"; 9 | import Auth from "./pages/Auth"; 10 | import ProfessorDetail from "./pages/ProfessorDetails"; 11 | import { useAuth } from "@clerk/clerk-react"; 12 | import { useNavigate } from 'react-router-dom' 13 | 14 | 15 | function App() { 16 | const [isAuthenticated, setIs] = useState(false) 17 | const {userId, isLoaded} =useAuth() 18 | const navigate = useNavigate() 19 | console.log('userID',userId) 20 | 21 | useEffect(() => { 22 | if (isLoaded && !userId) { 23 | navigate('/auth') 24 | } 25 | }, [isLoaded]) 26 | return ( 27 | 28 | 29 | } 32 | /> 33 | 34 | } /> 35 | 36 | 37 | }/> 40 | 41 | 42 | 43 | ); 44 | } 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { 3 | SignedIn, 4 | SignedOut, 5 | SignInButton, 6 | SignUpButton, 7 | UserButton, 8 | } from "@clerk/clerk-react"; 9 | import axios from "axios"; 10 | 11 | import { RiInstagramLine } from "react-icons/ri"; 12 | import { FaFacebook } from "react-icons/fa"; 13 | import NavBar from "../components/NavBar"; 14 | 15 | const API_URL = "https://api.unsplash.com/photos/random"; 16 | 17 | function Auth() { 18 | const [backgroundImage, setBackgroundImage] = useState(""); 19 | 20 | useEffect(() => { 21 | const fetchImage = async () => { 22 | try { 23 | const res = await axios.get(API_URL, { 24 | headers: { 25 | Authorization: `Client-ID ${import.meta.env.VITE_API_KEY}`, 26 | }, 27 | params: { 28 | query: "campus", 29 | orientation: "landscape", 30 | }, 31 | }); 32 | console.log(res.data) 33 | setBackgroundImage(res.data.urls.full); 34 | } catch (error) { 35 | console.error("Error fetching background image:", error); 36 | } 37 | }; 38 | 39 | fetchImage(); 40 | }, []); 41 | 42 | return ( 43 |
44 |
45 | 46 |
47 | 48 |
49 | 54 |

Welcome to Our Platform

55 |

56 | Join us by signing in or create an account for the first to rate our 57 | amazing professors! 58 |

59 |
60 |
61 | ); 62 | } 63 | 64 | export default Auth; 65 | -------------------------------------------------------------------------------- /src/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SignedOut, SignedIn, useUser, SignInButton, SignOutButton, } from "@clerk/clerk-react"; 3 | import { useNavigate } from 'react-router-dom'; // Import useNavigate 4 | import "../css/NavBar.css"; 5 | import { RiInstagramLine } from "react-icons/ri"; 6 | import { FaFacebook } from "react-icons/fa"; 7 | import Auth from "../pages/Auth"; 8 | 9 | function NavBar() { 10 | const { user } = useUser(); 11 | const navigate = useNavigate(); // Get navigate function 12 | 13 | const handleSignOut = () => { 14 | navigate('/auth'); // Redirect to Auth page after signing out 15 | }; 16 | 17 | return ( 18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 | 26 | 30 | Sign In 31 | 32 | 33 | 34 | 35 | 36 | {user ? `Hi, ${user?.primaryEmailAddress?.emailAddress.split('@')[0]}` : "Hi, Guest"} 37 | 38 | 41 | Sign Out 42 | 43 | 44 |
45 |
46 | ); 47 | } 48 | 49 | export default NavBar; 50 | -------------------------------------------------------------------------------- /src/css/professorDetails.css: -------------------------------------------------------------------------------- 1 | /* ProfessorDetail.css */ 2 | 3 | .professor-details { 4 | width: 90%; 5 | max-width: 700px; 6 | margin: 20px auto; 7 | padding: 20px; 8 | background-color: #f7f9fc; 9 | border-radius: 8px; 10 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | .professor-details h2 { 14 | color: #333; 15 | font-size: 1.8em; 16 | margin-bottom: 5px; 17 | } 18 | 19 | .rating-summary { 20 | background-color: #e6f3ff; 21 | padding: 15px; 22 | border-radius: 6px; 23 | margin-bottom: 20px; 24 | } 25 | 26 | .rating-summary p { 27 | font-weight: bold; 28 | color: #444; 29 | } 30 | 31 | .rating-list h3 { 32 | color: #555; 33 | font-size: 1.4em; 34 | margin-bottom: 10px; 35 | } 36 | 37 | .rating-list ul { 38 | list-style-type: none; 39 | padding: 0; 40 | } 41 | 42 | .rating-list li { 43 | padding: 10px; 44 | margin-bottom: 10px; 45 | border-bottom: 1px solid #ddd; 46 | } 47 | 48 | .rating-list li p { 49 | margin: 5px 0; 50 | } 51 | 52 | .error-message { 53 | color: red; 54 | font-weight: bold; 55 | margin-bottom: 10px; 56 | } 57 | 58 | .add-rating { 59 | background-color: #f2f2f2; 60 | padding: 15px; 61 | border-radius: 6px; 62 | } 63 | 64 | .add-rating input[type="number"], 65 | .add-rating textarea { 66 | width: 100%; 67 | padding: 8px; 68 | border: 1px solid #ddd; 69 | border-radius: 4px; 70 | font-size: 1em; 71 | margin-bottom: 10px; 72 | } 73 | 74 | .add-rating .radio-group { 75 | display: flex; 76 | gap: 10px; 77 | align-items: center; 78 | } 79 | 80 | .add-rating textarea { 81 | resize: none; 82 | } 83 | 84 | .add-rating button { 85 | padding: 10px 15px; 86 | background-color: #007bff; 87 | color: white; 88 | border: none; 89 | border-radius: 4px; 90 | font-size: 1em; 91 | cursor: pointer; 92 | transition: background-color 0.3s ease; 93 | } 94 | 95 | .add-rating button:hover { 96 | background-color: #0056b3; 97 | } 98 | -------------------------------------------------------------------------------- /src/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import { useUser } from '@clerk/clerk-react'; 4 | import NavBar from '../components/NavBar'; 5 | import ProfessorDetail from './ProfessorDetails'; 6 | 7 | 8 | function HomePage() { 9 | const { user } = useUser(); 10 | const [professors, setProfessors] = useState([]); 11 | const [selectedProf, setSelectedProf] = useState(null); // Store selected professor details 12 | const [error, setError] = useState(null); 13 | const [loading, setLoading] = useState(true); 14 | 15 | useEffect(() => { 16 | const fetchProfessors = async () => { 17 | try { 18 | const res = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/api/professors`); 19 | setProfessors(res.data); 20 | } catch (error) { 21 | setError('Error fetching professors'); 22 | console.error(error); 23 | } finally { 24 | setLoading(false); 25 | } 26 | }; 27 | fetchProfessors(); 28 | }, []); 29 | 30 | const handleSelectProfessor = async (id) => { 31 | if (!id) return; // Prevent unnecessary API call when no professor is selected 32 | 33 | try { 34 | setLoading(true); 35 | const res = await axios.get(`${import.meta.env.VITE_API_BASE_URL}/api/professors/${id}`); 36 | setSelectedProf(res.data); 37 | setError(null); 38 | } catch (error) { 39 | setError('Error fetching professor details'); 40 | console.error(error); 41 | } finally { 42 | setLoading(false); 43 | } 44 | }; 45 | 46 | return ( 47 | <> 48 | 49 |
50 |

51 | Welcome, {user?.primaryEmailAddress?.emailAddress.split('@')[0] || 'Guest'}! to the Rate Your Professor app 52 |

53 | 54 | {loading &&

Loading...

} 55 | {error &&

{error}

} {/* You can style this class for better visibility */} 56 | 57 | 65 | 66 | {selectedProf && ( 67 | 68 | )} 69 |
70 | 71 | ); 72 | } 73 | 74 | export default HomePage; 75 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/ProfessorDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import axios from 'axios'; 3 | import '../css/ProfessorDetails.css'; 4 | 5 | function ProfessorDetail({ professor, onRatingAdded }) { 6 | const [newRating, setNewRating] = useState({ 7 | rating: '', 8 | difficulty: '', 9 | wouldTakeAgain: '', 10 | comment: '', 11 | }); 12 | const [error, setError] = useState(null); 13 | 14 | const handleChange = (e) => { 15 | const { name, value } = e.target; 16 | setNewRating((prev) => ({ ...prev, [name]: value })); 17 | }; 18 | 19 | const handleAddRating = async () => { 20 | try { 21 | await axios.post(`${import.meta.env.VITE_API_BASE_URL}/api/ratings`, { 22 | ...newRating, 23 | professor: professor._id, 24 | }); 25 | onRatingAdded(professor._id); 26 | 27 | setNewRating({ 28 | rating: '', 29 | difficulty: '', 30 | wouldTakeAgain: '', 31 | comment: '', 32 | }); 33 | setError(null); 34 | } catch (error) { 35 | setError('Error adding rating'); 36 | console.error(error); 37 | } 38 | }; 39 | 40 | const getRatingSummary = () => { 41 | const totalRatings = professor.ratings.length; 42 | if (totalRatings === 0) return { averageRating: 0, averageDifficulty: 0, percentWouldTakeAgain: 0 }; 43 | 44 | const totalRatingValue = professor.ratings.reduce((acc, rating) => acc + rating.rating, 0); 45 | const totalDifficultyValue = professor.ratings.reduce((acc, rating) => acc + rating.difficulty, 0); 46 | const wouldTakeAgainCount = professor.ratings.filter(rating => rating.wouldTakeAgain === 'yes').length; 47 | 48 | return { 49 | averageRating: (totalRatingValue / totalRatings).toFixed(2), 50 | averageDifficulty: (totalDifficultyValue / totalRatings).toFixed(2), 51 | percentWouldTakeAgain: ((wouldTakeAgainCount / totalRatings) * 100).toFixed(0), 52 | }; 53 | }; 54 | 55 | const { averageRating, averageDifficulty, percentWouldTakeAgain } = getRatingSummary(); 56 | 57 | return ( 58 |
59 |

{professor.name} - {professor.department}

60 | 61 |
62 |

Ratings Summary:

63 |

Average Rating: {averageRating}

64 |

Average Difficulty: {averageDifficulty}

65 |

Would Take Again: {percentWouldTakeAgain}%

66 |
67 | 68 |
69 |

Ratings:

70 |
    71 | {professor.ratings.map((rating) => ( 72 |
  • 73 |

    Rating: {rating.rating}

    74 |

    Difficulty: {rating.difficulty}

    75 |

    Would Take Again: {rating.wouldTakeAgain === 'yes' ? 'Yes' : 'No'}

    76 |

    Comment: {rating.comment}

    77 |
  • 78 | ))} 79 |
80 |
81 | 82 |
83 |

Add a New Rating:

84 | {error &&

{error}

} 85 | 86 | 96 | 106 | 107 |

Would Take Again:

108 |
109 | 119 | 129 |
130 | 131 |