├── 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 |
140 |
141 | );
142 | }
143 |
144 | export default ProfessorDetail;
145 |
--------------------------------------------------------------------------------