diff --git a/src/App.tsx b/src/App.tsx index 274d4012..18097bbc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,100 +6,108 @@ import Navbar from './components/Navbar'; import ListPage from './pages/ListPage'; import MapPage from './pages/MapPage'; import NotFoundPage from './pages/NotFoundPage'; +import ReviewFormPage from './pages/ReviewFormPage'; // Import the ReviewFormPage +import RatingPage from './pages/RatingFormPage'; // Import the new RatingPage import { queryLocations, getLocationStatus } from './util/queryLocations'; import './App.css'; import { - IReadOnlyExtendedLocation, - IReadOnlyLocation, + IReadOnlyExtendedLocation, + IReadOnlyLocation, } from './types/locationTypes'; const CMU_EATS_API_URL = 'https://dining.apis.scottylabs.org/locations'; -// const CMU_EATS_API_URL = 'http://localhost:5173/example-response.json'; // for debugging purposes (note that you need an example-response.json file in the /public folder) -// const CMU_EATS_API_URL = 'http://localhost:5010/locations'; // for debugging purposes (note that you need an example-response.json file in the /public folder) + function App() { - // Load locations - const [locations, setLocations] = useState(); - const [extendedLocationData, setExtendedLocationData] = - useState(); - useEffect(() => { - queryLocations(CMU_EATS_API_URL).then((parsedLocations) => { - setLocations(parsedLocations); - }); - }, []); + // Load locations + const [locations, setLocations] = useState(); + const [extendedLocationData, setExtendedLocationData] = + useState(); + + useEffect(() => { + queryLocations(CMU_EATS_API_URL).then((parsedLocations) => { + setLocations(parsedLocations); + }); + }, []); - useEffect(() => { - const intervalId = setInterval( - (function updateExtendedLocationData() { - if (locations !== undefined) { - // Remove .setZone('America/New_York') and change time in computer settings when testing - // Alternatively, simply set now = DateTime.local(2023, 12, 22, 18, 33); where the parameters are Y,M,D,H,M - const now = DateTime.now().setZone('America/New_York'); - setExtendedLocationData( - locations.map((location) => ({ - ...location, - ...getLocationStatus(location.times, now), // populate location with more detailed info relevant to current time - })), - ); - } - return updateExtendedLocationData; // returns itself here - })(), // self-invoking function - 1 * 1000, // updates every second - ); - return () => clearInterval(intervalId); - }, [locations]); + useEffect(() => { + const intervalId = setInterval( + (function updateExtendedLocationData() { + if (locations !== undefined) { + const now = DateTime.now().setZone('America/New_York'); + setExtendedLocationData( + locations.map((location) => ({ + ...location, + ...getLocationStatus(location.times, now), // populate location with more detailed info relevant to current time + })) + ); + } + return updateExtendedLocationData; + })(), + 1 * 1000, // updates every second + ); + return () => clearInterval(intervalId); + }, [locations]); - // Auto-refresh the page when the user goes online after previously being offline - useEffect(() => { - function handleOnline() { - if (navigator.onLine) { - // Refresh the page - window.location.reload(); - } - } + // Auto-refresh the page when the user goes online after previously being offline + useEffect(() => { + function handleOnline() { + if (navigator.onLine) { + // Refresh the page + window.location.reload(); + } + } - window.addEventListener('online', handleOnline); + window.addEventListener('online', handleOnline); - return () => window.removeEventListener('online', handleOnline); - }, []); + return () => window.removeEventListener('online', handleOnline); + }, []); - return ( - - -
-
- Pre-register for{' '} - - TartanHacks - - , Pittsburgh's LARGEST hackathon! 🖥️ -
-
- - - } - /> - - } - /> - } /> - -
- -
-
-
- ); + return ( + + +
+
+ Pre-register for{' '} + + TartanHacks + + , Pittsburgh's LARGEST hackathon! 🖥️ +
+
+ + + } + /> + + } + /> + } // Add the new ReviewFormPage route + /> + } // New route for RatingPage + /> + } /> + +
+ +
+
+
+ ); } export default App; diff --git a/src/components/EateryCard.tsx b/src/components/EateryCard.tsx index 5c570aec..7a5afd79 100644 --- a/src/components/EateryCard.tsx +++ b/src/components/EateryCard.tsx @@ -1,4 +1,5 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; + import { Card, CardHeader, @@ -164,6 +165,7 @@ const SpecialsContent = styled(Accordion)({ backgroundColor: '#23272A', }); + function EateryCard({ location }: { location: IReadOnlyExtendedLocation }) { const { name, @@ -177,9 +179,33 @@ function EateryCard({ location }: { location: IReadOnlyExtendedLocation }) { } = location; const changesSoon = !location.closedLongTerm && location.changesSoon; const isOpen = !location.closedLongTerm && location.isOpen; - const [modalOpen, setModalOpen] = useState(false); + + const [averageRatings, setAverageRatings] = useState<{ + overall: number; + }>({ overall: 0 }); // Default values + + useEffect(() => { + const fetchAverageRating = async () => { + try { + const response = await fetch(`/api/ratings/average/${name}`); + if (response.ok) { + const data = await response.json(); + setAverageRatings(data.averageRating || { overall: 0 }); + } else { + console.warn(`No ratings available for ${name}`); + } + } catch (error) { + console.error('Error fetching average ratings:', error); + } + }; + + fetchAverageRating(); + }, [name]); + + + return ( <> @@ -227,6 +253,9 @@ function EateryCard({ location }: { location: IReadOnlyExtendedLocation }) { {locationText} {shortDescription} + + Average Rating: {averageRatings.overall ? averageRatings.overall.toFixed(1) : 'Not Rated'} + {menu && ( diff --git a/src/components/Navbar.css b/src/components/Navbar.css index 3b605ca6..d4f383ef 100644 --- a/src/components/Navbar.css +++ b/src/components/Navbar.css @@ -1,52 +1,48 @@ .Navbar { height: var(--navbar-height); - background-color: #1e1e1e; - padding: 14px; - box-sizing: border-box; - border-top: 2px solid #31373e; + background-color: #1e1e1e; + padding: 14px; + box-sizing: border-box; + border-top: 2px solid #31373e; } .Navbar-links { position: relative; - display: grid; - grid-auto-columns: 1fr; - grid-auto-flow: column; - height: 40px; - margin: 0 auto; - max-width: 500px; + display: grid; + grid-template-columns: repeat(4, 1fr); /* Adjusted for 4 links now */ + grid-auto-flow: column; + height: 40px; + margin: 0 auto; + max-width: 600px; /* Adjusted width to accommodate new link */ } .Navbar-links a { position: relative; - z-index: 5; - display: flex; - align-items: center; - justify-content: center; - font-family: 'Zilla Slab', sans-serif; - font-weight: 500; - color: white; - font-size: 16px; - text-decoration: none; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + font-family: 'Zilla Slab', sans-serif; + font-weight: 500; + color: white; + font-size: 16px; + text-decoration: none; } .Navbar-links svg { width: 24px; - height: 24px; - margin-right: 0.4em; + height: 24px; + margin-right: 0.4em; } .Navbar-active { - position: absolute; - z-index: 2; - left: 0; - top: 0; - width: 50%; - height: 100%; - background-color: #2b2f33; - border-radius: 999px; - transition: - transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), - box-shadow 0.3s ease-in-out; + position: absolute; + background-color: #2b2f33; + border-radius: 999px; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 5px #ee6f52, 0 0 10px #ee6f52, 0 0 15px #ee6f52; /* Glowing effect */ + width: calc(350% / 4); /* Adapt width to the number of links */ + height: 100%; } .Navbar-active_map { diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 996a983a..fb828672 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,57 +1,49 @@ -import { Link, useLocation } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import './Navbar.css'; +import StarOutlineIcon from '@mui/icons-material/StarOutline'; // Import the empty star icon function Navbar() { - const location = useLocation(); - - return ( - - ); + return ( + + ); } export default Navbar; diff --git a/src/pages/RatingFormPage.tsx b/src/pages/RatingFormPage.tsx new file mode 100644 index 00000000..e6edde2c --- /dev/null +++ b/src/pages/RatingFormPage.tsx @@ -0,0 +1,381 @@ +import React, { useState } from 'react'; +import { Typography, Button, RadioGroup, FormControlLabel, Radio, TextField, Snackbar } from '@mui/material'; +import MuiAlert from '@mui/material/Alert'; + + +const RatingPage = () => { + const [userEmail, setUserEmail] = useState(''); // Email state + const [foodRating, setFoodRating] = useState(3); // Food rating state + const [locationRating, setLocationRating] = useState(3); // Location rating state + const [cleanlinessRating, setCleanlinessRating] = useState(3); // Cleanliness rating state + const [serviceRating, setServiceRating] = useState(3); // Service rating state + const [valueForMoneyRating, setValueForMoneyRating] = useState(3); // Value for money rating state + const [menuVarietyRating, setMenuVarietyRating] = useState(3); // Menu variety rating state + const [waitTimeRating, setWaitTimeRating] = useState(3); // Wait time rating state + const [staffRating, setStaffRating] = useState(3); // Staff rating state + const [overallSatisfactionRating, setOverallSatisfactionRating] = useState(3); // Overall satisfaction rating state + const [selectedRestaurant, setSelectedRestaurant] = useState(''); // Restaurant selection state + const [success, setSuccess] = useState(false); // Success notification state + + + // Define textFieldStyles for styling + const textFieldStyles = { + backgroundColor: '#2D2F32', + '& label.Mui-focused': { + color: 'green', + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: 'white', + }, + '&:hover fieldset': { + borderColor: 'green', + }, + '&.Mui-focused fieldset': { + borderColor: 'green', + }, + }, + }; + + // Define the handleSubmit function + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const ratings = { + userEmail, + restaurantName: selectedRestaurant, // Use name instead of ID + foodRating, + locationRating, + cleanlinessRating, + serviceRating, + valueForMoneyRating, + menuVarietyRating, + waitTimeRating, + staffRating, + overallSatisfactionRating, + }; + + try { + const response = await fetch('/api/ratings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(ratings), + }); + + if (response.ok) { + setSuccess(true); // Show success notification + } else { + console.error('Failed to submit rating'); + } + } catch (error) { + console.error('Error submitting rating:', error); + } + }; + + const handleSnackbarClose = () => setSuccess(false); + + return ( +
+
+ + Rate Our Services + + + Thank you for dining with us! We appreciate the time you’ve taken to share your feedback with us. Your thoughts help us continue to improve and provide you with the best possible experience. + +
+ +
+ {/* Email Input */} + setUserEmail(e.target.value)} + /> + + {/* Restaurant Selection */} + setSelectedRestaurant(e.target.value)} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Food Rating */} +
+ + How would you rate the food? (1 = Poor, 5 = Excellent) + + setFoodRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Location Rating */} +
+ + How would you rate the location? (1 = Poor, 5 = Excellent) + + setLocationRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + + {/* Cleanliness Rating */} +
+ + How would you rate the cleanliness? (1 = Poor, 5 = Excellent) + + setCleanlinessRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Service Rating */} +
+ + How would you rate the service? (1 = Poor, 5 = Excellent) + + setServiceRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Value for Money */} +
+ + How would you rate the value for money of your meal? (1 = Poor, 5 = Excellent) + + setValueForMoneyRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Menu Variety Rating */} +
+ + How satisfied were you with the variety of options on the menu? (1 = Poor, 5 = Excellent) + + setMenuVarietyRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Wait Time Rating */} +
+ + How would you rate the wait time for your food? (1 = Too long, 5 = Perfect) + + setWaitTimeRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Staff Rating */} +
+ + How friendly and welcoming was the staff? (1 = Not friendly, 5 = Very friendly) + + setStaffRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Overall Satisfaction Rating */} +
+ + Overall, how satisfied were you with your experience? (1 = Very dissatisfied, 5 = Very satisfied) + + setOverallSatisfactionRating(parseInt(e.target.value))} + row + > + {[1, 2, 3, 4, 5].map((value) => ( + } + label={value} + labelPlacement="bottom" + /> + ))} + +
+ + {/* Submit Button */} + + + + {/* Snackbar Notification */} + + + Successfully submitted your review! + + +
+ ); +}; + + +export default RatingPage; diff --git a/src/pages/ReviewFormPage.tsx b/src/pages/ReviewFormPage.tsx new file mode 100644 index 00000000..9e395250 --- /dev/null +++ b/src/pages/ReviewFormPage.tsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +import { Typography, Button, TextField, Checkbox, FormControlLabel } from '@mui/material'; + +const RatingFormPage = () => { + // Defined textFieldStyles + const textFieldStyles = { + backgroundColor: '#2D2F32', // Adjust the background color if needed + '& label.Mui-focused': { + color: 'green', // Changes the label color to green when focused + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + borderColor: 'grey', // Default border color + }, + '&:hover fieldset': { + borderColor: 'green', // Border color changes to green on hover + }, + '&.Mui-focused fieldset': { + borderColor: 'green', // Border color when the input is focused + }, + } + }; + + const [userEmail, setUserEmail] = useState(''); + const [selectedRestaurant, setSelectedRestaurant] = useState(''); + const [wouldRecommend, setWouldRecommend] = useState(false); + const [foodExperience, setFoodExperience] = useState(''); + const [serviceExperience, setServiceExperience] = useState(''); + const [cleanlinessExperience, setCleanlinessExperience] = useState(''); + const [additionalComments, setAdditionalComments] = useState(''); + + return ( +
+
+ + Leave a Review + +
+
+ {/* Email Input */} + setUserEmail(e.target.value)} + /> + + {/* Restaurant Selection */} + setSelectedRestaurant(e.target.value)} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Food Experience */} + setFoodExperience(e.target.value)} + /> + + {/* Service Experience */} + setServiceExperience(e.target.value)} + /> + + {/* Cleanliness Experience */} + setCleanlinessExperience(e.target.value)} + /> + +{/* Recommend Checkbox */} + setWouldRecommend(e.target.checked)} + /> + } + label="Would you recommend this restaurant to a friend?" + /> + + {/* Additional Comments */} + setAdditionalComments(e.target.value)} + /> + + {/* Submit Button */} + + +
+ ); +}; + +export default RatingFormPage; diff --git a/src/services/ratingModel.d.ts b/src/services/ratingModel.d.ts new file mode 100644 index 00000000..47687be1 --- /dev/null +++ b/src/services/ratingModel.d.ts @@ -0,0 +1,29 @@ +export interface Rating { + userEmail: string; + restaurantName: string; + foodRating: number; + locationRating: number; + cleanlinessRating: number; + serviceRating: number; + valueForMoneyRating: number; + menuVarietyRating: number; + waitTimeRating: number; + staffRating: number; + overallSatisfactionRating: number; +} + +export declare const RatingModel: { + create(ratingData: Rating): Promise; + getRatingsByRestaurant(restaurantName: string): Promise; + getAverageRating(restaurantName: string): Promise<{ + foodRating: number; + locationRating: number; + cleanlinessRating: number; + serviceRating: number; + valueForMoneyRating: number; + menuVarietyRating: number; + waitTimeRating: number; + staffRating: number; + overallSatisfactionRating: number; + } | null>; +}; \ No newline at end of file diff --git a/src/services/ratingModel.js b/src/services/ratingModel.js new file mode 100644 index 00000000..3ec1dec7 --- /dev/null +++ b/src/services/ratingModel.js @@ -0,0 +1,64 @@ +/** + * @typedef {Object} Rating + * @property {string} userEmail - The user's email. + * @property {string} restaurantName - The name of the restaurant. + * @property {number} foodRating - Rating for food. + * @property {number} locationRating - Rating for location. + * @property {number} cleanlinessRating - Rating for cleanliness. + * @property {number} serviceRating - Rating for service. + * @property {number} valueForMoneyRating - Rating for value for money. + * @property {number} menuVarietyRating - Rating for menu variety. + * @property {number} waitTimeRating - Rating for wait time. + * @property {number} staffRating - Rating for staff. + * @property {number} overallSatisfactionRating - Rating for overall satisfaction. + */ + +/** Simulated in-memory database */ +const ratingsDb = []; + +/** RatingModel provides methods to interact with the ratings database */ +export const RatingModel = { + /** + * Add a new rating. + * @param {Rating} ratingData - The rating data to be added. + * @returns {Promise} The added rating data. + */ + create: async (ratingData) => { + ratingsDb.push(ratingData); + return ratingData; + }, + + /** + * Get all ratings for a specific restaurant. + * @param {string} restaurantName - The name of the restaurant. + * @returns {Promise} List of ratings for the restaurant. + */ + getRatingsByRestaurant: async (restaurantName) => { + return ratingsDb.filter((rating) => rating.restaurantName === restaurantName); + }, + + /** + * Get the average ratings for a specific restaurant. + * @param {string} restaurantName - The name of the restaurant. + * @returns {Promise} The average ratings or null if no ratings exist. + */ + getAverageRating: async (restaurantName) => { + const restaurantRatings = ratingsDb.filter((rating) => rating.restaurantName === restaurantName); + if (restaurantRatings.length === 0) return null; + + const totalRatings = restaurantRatings.length; + const averageRating = { + foodRating: restaurantRatings.reduce((sum, rating) => sum + rating.foodRating, 0) / totalRatings, + locationRating: restaurantRatings.reduce((sum, rating) => sum + rating.locationRating, 0) / totalRatings, + cleanlinessRating: restaurantRatings.reduce((sum, rating) => sum + rating.cleanlinessRating, 0) / totalRatings, + serviceRating: restaurantRatings.reduce((sum, rating) => sum + rating.serviceRating, 0) / totalRatings, + valueForMoneyRating: restaurantRatings.reduce((sum, rating) => sum + rating.valueForMoneyRating, 0) / totalRatings, + menuVarietyRating: restaurantRatings.reduce((sum, rating) => sum + rating.menuVarietyRating, 0) / totalRatings, + waitTimeRating: restaurantRatings.reduce((sum, rating) => sum + rating.waitTimeRating, 0) / totalRatings, + staffRating: restaurantRatings.reduce((sum, rating) => sum + rating.staffRating, 0) / totalRatings, + overallSatisfactionRating: restaurantRatings.reduce((sum, rating) => sum + rating.overallSatisfactionRating, 0) / totalRatings, + }; + + return averageRating; + }, +}; \ No newline at end of file diff --git a/src/services/ratingRoutes.ts b/src/services/ratingRoutes.ts new file mode 100644 index 00000000..31a28b39 --- /dev/null +++ b/src/services/ratingRoutes.ts @@ -0,0 +1,29 @@ +import { RatingModel } from './ratingModel.js'; + +export const ratingRoutes = (req: Request) => { + const url = new URL(req.url); + const method = req.method; + + if (url.pathname === '/api/ratings' && method === 'POST') { + return req.json().then(async (ratingData) => { + const newRating = await RatingModel.create(ratingData); + return new Response(JSON.stringify(newRating), { status: 200 }); + }); + } + + if (url.pathname.startsWith('/api/ratings/average/') && method === 'GET') { + const restaurantName = url.pathname.split('/').pop(); + return RatingModel.getAverageRating(restaurantName || '').then((averageRating) => + new Response(JSON.stringify({ averageRating }), { status: 200 }) + ); + } + + if (url.pathname.startsWith('/api/ratings/') && method === 'GET') { + const restaurantName = url.pathname.split('/').pop(); + return RatingModel.getRatingsByRestaurant(restaurantName || '').then((ratings) => + new Response(JSON.stringify(ratings), { status: 200 }) + ); + } + + return Promise.resolve(new Response('Not Found', { status: 404 })); +}; diff --git a/src/util/server.ts b/src/util/server.ts new file mode 100644 index 00000000..a5115288 --- /dev/null +++ b/src/util/server.ts @@ -0,0 +1,14 @@ +import { ratingRoutes } from '../services/ratingRoutes'; + +const server = Bun.serve({ + port: 3000, + async fetch(req) { + // Delegate to ratingRoutes for handling API requests + if (req.url.startsWith('/api/ratings')) { + return ratingRoutes(req); + } + return new Response('Not Found', { status: 404 }); + }, +}); + +console.log(`Server running on http://localhost:${server.port}`); \ No newline at end of file diff --git a/tests/ratingModel.test.ts b/tests/ratingModel.test.ts new file mode 100644 index 00000000..5c5eaace --- /dev/null +++ b/tests/ratingModel.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, vi } from 'vitest'; + +// Mock the RatingModel +vi.mock('../src/services/ratingModel', () => ({ + RatingModel: { + create: vi.fn().mockResolvedValue({ restaurant: 'Mock Restaurant', rating: 5, comment: 'Mock Comment' }), + getRatingsByRestaurant: vi.fn().mockResolvedValue([{ restaurant: 'Mock Restaurant', rating: 5 }]), + getAverageRating: vi.fn().mockResolvedValue(4.5), + }, +})); + +import { RatingModel } from '../src/services/ratingModel'; + +describe('RatingModel Tests', () => { + it('should create a new rating', async () => { + const newRating = await RatingModel.create({ + restaurant: 'Test Restaurant', + rating: 4, + comment: 'Great!', + }); + expect(newRating).toMatchObject({ + restaurant: 'Mock Restaurant', + rating: 5, + comment: 'Mock Comment', + }); + }); + + it('should fetch ratings by restaurant name', async () => { + const ratings = await RatingModel.getRatingsByRestaurant('Test Restaurant'); + expect(ratings).toBeInstanceOf(Array); + expect(ratings.length).toBeGreaterThan(0); + expect(ratings[0]).toMatchObject({ restaurant: 'Mock Restaurant', rating: 5 }); + }); + + it('should calculate the average rating for a restaurant', async () => { + const average = await RatingModel.getAverageRating('Test Restaurant'); + expect(average).toBe(4.5); + }); +}); diff --git a/tests/ratingRoutes.test.ts b/tests/ratingRoutes.test.ts new file mode 100644 index 00000000..2ba95abd --- /dev/null +++ b/tests/ratingRoutes.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import request from 'supertest'; +import app from '../src/app'; // Replace with the correct path to your Express app + +describe('Rating Routes Tests', () => { + it('should create a new rating via POST /api/ratings', async () => { + const response = await request(app).post('/api/ratings').send({ + restaurant: 'Test Restaurant', + rating: 5, + comment: 'Excellent!', + }); + expect(response.status).toBe(201); + expect(response.body).toMatchObject({ + restaurant: 'Test Restaurant', + rating: 5, + comment: 'Excellent!', + }); + }); + + it('should fetch ratings via GET /api/ratings/:restaurantName', async () => { + const response = await request(app).get('/api/ratings/Test Restaurant'); + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Array); + }); + + it('should calculate the average rating via GET /api/ratings/average/:restaurantName', async () => { + const response = await request(app).get('/api/ratings/average/Test Restaurant'); + expect(response.status).toBe(200); + expect(response.body).toHaveProperty('averageRating'); + expect(response.body.averageRating).toBeGreaterThanOrEqual(1); + expect(response.body.averageRating).toBeLessThanOrEqual(5); + }); + it('should handle server errors gracefully', async () => { + // Simulate a database error + vi.spyOn(RatingModel, 'getRatingsByRestaurant').mockImplementation(() => { + throw new Error('Database error'); + }); + + const response = await request(app).get('/api/ratings/Test Restaurant'); + expect(response.status).toBe(500); + expect(response.body).toHaveProperty('error', 'Failed to fetch ratings'); + }); +});