Let's create a detailed plan for a "Yelp-type" service rating application.
This application will allow users to view, add, update, and delete reviews for different services.
We'll use a Node.js server to simulate the database with object literals and build a client-side application to interact with this server.
### **Magnus Opus Project: Yelp-Type Service Rating App**
#### **Objective:**
- Implement a full-featured web application that allows users to perform CRUD RESTful API operations on service reviews.
- Use a Node.js server to simulate a database with object literals.
- Create a client-side application to interact with the server.
---
Part 1: Setting Up the Node.js Server**
#### **1. Initialize the Project**
1. Create a new directory for the project and navigate into it:
```bash
mkdir yelp-type-app
cd yelp-type-app
```
2. Initialize a new Node.js project:
```bash
npm init -y
```
3. Install the required dependencies:
```bash
npm install express
```
2. Create the Server**
1. Create a file named `server.js`:
```javascript
const express = require('express');
const app = express();
const port = 3000;
app.use(express.json());
// Simulated JSON database using object literals
let services = [
{ id: 1, name: 'Coffee Shop', rating: 4.5, reviews: [{ id: 1, text: 'Great coffee!', rating: 5 }] },
{ id: 2, name: 'Book Store', rating: 4.0, reviews: [{ id: 1, text: 'Nice selection of books.', rating: 4 }] },
];
// Create a new service
app.post('/services', (req, res) => {
const newService = {
id: services.length + 1,
name: req.body.name,
rating: 0,
reviews: []
};
services.push(newService);
res.status(201).json(newService);
});
// Get all services
app.get('/services', (req, res) => {
res.json(services);
});
// Get a single service
app.get('/services/:id', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.id));
if (!service) return res.status(404).send('Service not found');
res.json(service);
});
// Add a review to a service
app.post('/services/:id/reviews', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.id));
if (!service) return res.status(404).send('Service not found');
const newReview = {
id: service.reviews.length + 1,
text: req.body.text,
rating: req.body.rating
};
service.reviews.push(newReview);
service.rating = (service.reviews.reduce((sum, review) => sum + review.rating, 0) / service.reviews.length).toFixed(1);
res.status(201).json(newReview);
});
// Update a review
app.put('/services/:serviceId/reviews/:reviewId', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.serviceId));
if (!service) return res.status(404).send('Service not found');
const review = service.reviews.find(r => r.id === parseInt(req.params.reviewId));
if (!review) return res.status(404).send('Review not found');
review.text = req.body.text;
review.rating = req.body.rating;
service.rating = (service.reviews.reduce((sum, review) => sum + review.rating, 0) / service.reviews.length).toFixed(1);
res.json(review);
});
// Delete a review
app.delete('/services/:serviceId/reviews/:reviewId', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.serviceId));
if (!service) return res.status(404).send('Service not found');
const reviewIndex = service.reviews.findIndex(r => r.id === parseInt(req.params.reviewId));
if (reviewIndex === -1) return res.status(404).send('Review not found');
service.reviews.splice(reviewIndex, 1);
service.rating = service.reviews.length > 0 ? (service.reviews.reduce((sum, review) => sum + review.rating, 0) / service.reviews.length).toFixed(1) : 0;
res.status(204).send();
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
```
@challenge: Add login security so users can only edit reviews they have written
2. Run the server:
```bash
node server.js
```
#### **Explanation to Students:**
- Explain the use of Express for setting up the server.
- Discuss the use of object literals to simulate a JSON database.
- Walk through each CRUD endpoint and its implementation.
---
### **Part 2: Creating the Client-Side Application**
#### **1. Create the HTML and CSS Files**
1. Create a file named `index.html`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yelp-Type Service Rating App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>Service Rating App</h1>
<h2>Add New Service</h2>
<input type="text" id="newServiceName" placeholder="Service Name">
<button onclick="addService()">Add Service</button>
<h2>Services</h2>
<div id="servicesList"></div>
<div id="serviceDetails" style="display: none;">
<h2 id="serviceName"></h2>
<p>Rating: <span id="serviceRating"></span></p>
<h3>Reviews</h3>
<ul id="reviewsList"></ul>
<h3>Add Review</h3>
<input type="text" id="newReviewText" placeholder="Review Text">
<input type="number" id="newReviewRating" placeholder="Rating" min="1" max="5">
<button onclick="addReview()">Add Review</button>
</div>
</div>
<script src="script.js"></script>
</body>
</html>
```
2. Create a file named `styles.css`:
```css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
.container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
input, button {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 10px;
}
button {
background-color: #4CAF50;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
```
2. Create the client side JavaScript File for use by the index.html page:
1. Create a file named `script.js`:
```javascript
document.addEventListener('DOMContentLoaded', () => {
getServices();
});
function getServices() {
fetch('/services')
.then(response => response.json())
.then(data => {
const servicesList = document.getElementById('servicesList');
servicesList.innerHTML = '';
data.forEach(service => {
const serviceDiv = document.createElement('div');
serviceDiv.innerHTML = `<h3>${service.name}</h3>
<p>Rating: ${service.rating}</p>
<button onclick="viewService(${service.id})">View Details</button>`;
servicesList.appendChild(serviceDiv);
});
})
.catch(error => console.error('Error:', error));
}
function addService() {
const name = document.getElementById('newServiceName').value;
fetch('/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name })
})
.then(response => response.json())
.then(data => {
alert(`Service added: ${data.name}`);
getServices();
document.getElementById('newServiceName').value = '';
})
.catch(error => console.error('Error:', error));
}
function viewService(id) {
fetch(`/services/${id}`)
.then(response => response.json())
.then(data => {
document.getElementById('serviceName').textContent = data.name;
document.getElementById('serviceRating').textContent = data.rating;
document.getElementById('serviceDetails').style.display = 'block';
const reviewsList = document.getElementById('reviewsList');
reviewsList.innerHTML = '';
data.reviews.forEach(review => {
const reviewLi = document.createElement('li');
reviewLi.textContent = `Rating: ${review.rating} - ${review.text}`;
reviewsList.appendChild(reviewLi);
});
})
.catch(error => console.error('Error:', error));
}
function addReview() {
const serviceId = document.getElementById('serviceName').dataset.id;
const text = document.getElementById('newReviewText').value;
const rating = document.getElementById('newReviewRating').value;
fetch(`/services/${serviceId}/reviews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text, rating })
})
.then(response => response.json())
.then(data => {
alert(`Review added: ${data.text}`);
viewService(serviceId);
document.getElementById('newReviewText').value = '';
document.getElementById('newReviewRating').value = '';
})
.catch(error => console.error('Error:', error));
}
```
#### **Explanation to Students:**
- Walk through the HTML structure, focusing on the forms and buttons for each CRUD operation.
- Explain the CSS for basic styling.
- Discuss the JavaScript functions that interact with the server using the Fetch API.
- Highlight how the client-side application updates the DOM based on user interactions and server responses.
Part 3: Running and Testing the Project
1. Ensure the server is running:
```bash
node server.js
```
2. Open the `index.html` file in a browser (you can use a local server like `http-server` for this):
```bash
npx http-server -c-1 -o
```
3. Test each functionality:
- Add a new service.
- View the list of services.
- View details of a specific service.
- Add reviews to a service.
- Update a review (implement this as an additional exercise).
- Delete a review (implement this as an additional exercise).
---
### **Conclusion:**
- Recap the RESTful principles demonstrated: stateless interactions, use of standard HTTP methods, and structured URLs.
- Highlight how the client-side application interacts with the server.
- Discuss potential improvements, such as form validation, better error handling, and UI enhancements.
#### **Extensions and Future Enhancements:**
- **Form Validation:** Add client-side validation to ensure all inputs are filled out correctly.
- **Error Handling:** Improve error handling to provide more detailed feedback to users.
- **UI Enhancements:** Enhance the user interface with better styling and user experience features.
- **Backend Improvements:** Introduce a real database, such as MongoDB, to store services and reviews.
This comprehensive project provides a hands-on approach to understanding and implementing REST APIs, making it practical and engaging for students.
To resolve the issue with the "Add Review" functionality not working, let's check and update the relevant code. The main points to verify are:
1. **Ensure the review submission is correctly associated with the selected service.**
2. **Ensure the correct function is called to add the review.**
3. **Ensure the service ID is correctly passed and used in the fetch request.**
Here’s a detailed step-by-step process to debug and fix the issue:
### 1. Verify HTML and Ensure Proper IDs and Classes
Ensure the HTML structure is correctly set up, especially the parts where reviews are added and displayed. We also need to ensure that the `data-id` attribute for the selected service is properly set so it can be used in the `addReview` function.
**`public/index.html`**:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Rating App</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="container">
<h1>Service Rating App</h1>
<h2>Add New Service</h2>
<input type="text" id="newServiceName" placeholder="Service Name">
<button onclick="addService()">Add Service</button>
<h2>Services</h2>
<div id="servicesList"></div>
<div id="serviceDetails" style="display: none;">
<h2 id="serviceName" data-id=""></h2>
<p>Rating: <span id="serviceRating"></span></p>
<h3>Reviews</h3>
<ul id="reviewsList"></ul>
<h3>Add Review</h3>
<input type="text" id="newReviewText" placeholder="Review Text">
<input type="number" id="newReviewRating" placeholder="Rating" min="1" max="5">
<button onclick="addReview()">Add Review</button>
</div>
</div>
<script src="/script.js"></script>
</body>
</html>
```
### 2. Update JavaScript Functions
Ensure the JavaScript functions correctly fetch and display the data. The `addReview` function must correctly use the service ID stored in the `data-id` attribute of the `serviceName` element.
**`public/js/script.js`**:
```javascript
document.addEventListener('DOMContentLoaded', () => {
getServices();
});
function getServices() {
fetch('http://localhost:3000/services')
.then(response => response.json())
.then(data => {
const servicesList = document.getElementById('servicesList');
servicesList.innerHTML = '';
data.forEach(service => {
const serviceDiv = document.createElement('div');
serviceDiv.innerHTML = `<h3>${service.name}</h3>
<p>Rating: ${service.rating}</p>
<button onclick="viewService(${service.id})">View Details</button>`;
servicesList.appendChild(serviceDiv);
});
})
.catch(error => console.error('Error:', error));
}
function addService() {
const name = document.getElementById('newServiceName').value;
fetch('http://localhost:3000/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name })
})
.then(response => response.json())
.then(data => {
alert(`Service added: ${data.name}`);
getServices();
document.getElementById('newServiceName').value = '';
})
.catch(error => console.error('Error:', error));
}
function viewService(id) {
fetch(`http://localhost:3000/services/${id}`)
.then(response => response.json())
.then(data => {
document.getElementById('serviceName').textContent = data.name;
document.getElementById('serviceName').setAttribute('data-id', data.id);
document.getElementById('serviceRating').textContent = data.rating;
document.getElementById('serviceDetails').style.display = 'block';
const reviewsList = document.getElementById('reviewsList');
reviewsList.innerHTML = '';
data.reviews.forEach(review => {
const reviewLi = document.createElement('li');
reviewLi.textContent = `Rating: ${review.rating} - ${review.text}`;
reviewsList.appendChild(reviewLi);
});
})
.catch(error => console.error('Error:', error));
}
function addReview() {
const serviceId = document.getElementById('serviceName').getAttribute('data-id');
const text = document.getElementById('newReviewText').value;
const rating = document.getElementById('newReviewRating').value;
fetch(`http://localhost:3000/services/${serviceId}/reviews`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text, rating })
})
.then(response => response.json())
.then(data => {
alert(`Review added: ${data.text}`);
viewService(serviceId); // Refresh the service details to show the new review
document.getElementById('newReviewText').value = '';
document.getElementById('newReviewRating').value = '';
})
.catch(error => console.error('Error:', error));
}
```
### 3. Ensure the Server Code Handles the Request
Ensure the server code correctly handles the requests for adding reviews.
**`server.js`**:
```javascript
const express = require('express');
const cors = require('cors');
const path = require('path');
const app = express();
const port = 3000;
app.use(express.json());
app.use(cors());
// Serve static files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public')));
let services = [
{ id: 1, name: 'Coffee Shop', rating: 4.5, reviews: [{ id: 1, text: 'Great coffee!', rating: 5 }] },
{ id: 2, name: 'Book Store', rating: 4.0, reviews: [{ id: 1, text: 'Nice selection of books.', rating: 4 }] },
];
// Root route to serve index.html
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Create a new service
app.post('/services', (req, res) => {
const newService = {
id: services.length + 1,
name: req.body.name,
rating: 0,
reviews: []
};
services.push(newService);
res.status(201).json(newService);
});
// Get all services
app.get('/services', (req, res) => {
res.json(services);
});
// Get a single service
app.get('/services/:id', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.id));
if (!service) return res.status(404).send('Service not found');
res.json(service);
});
// Add a review to a service
app.post('/services/:id/reviews', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.id));
if (!service) return res.status(404).send('Service not found');
const newReview = {
id: service.reviews.length + 1,
text: req.body.text,
rating: req.body.rating
};
service.reviews.push(newReview);
service.rating = (service.reviews.reduce((sum, review) => sum + review.rating, 0) / service.reviews.length).toFixed(1);
res.status(201).json(newReview);
});
// Update a review
app.put('/services/:serviceId/reviews/:reviewId', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.serviceId));
if (!service) return res.status(404).send('Service not found');
const review = service.reviews.find(r => r.id === parseInt(req.params.reviewId));
if (!review) return res.status(404).send('Review not found');
review.text = req.body.text;
review.rating = req.body.rating;
service.rating = (service.reviews.reduce((sum, review) => sum + review.rating, 0) / service.reviews.length).toFixed(1);
res.json(review);
});
// Delete a review
app.delete('/services/:serviceId/reviews/:reviewId', (req, res) => {
const service = services.find(s => s.id === parseInt(req.params.serviceId));
if (!service) return res.status(404).send('Service not found');
const reviewIndex = service.reviews.findIndex(r => r.id === parseInt(req.params.reviewId));
if (reviewIndex === -1) return res.status(404).send('Review not found');
service.reviews.splice(reviewIndex, 1);
service.rating = service.reviews.length > 0 ? (service.reviews.reduce((sum, review) => sum + review.rating, 0) / service.reviews.length).toFixed(1) : 0;
res.status(204).send();
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
```
###
Running and Testing
1. **Start the Server:**
```bash
node server.js
```
2. **Open the HTML File:**
Serve the `index.html` file using a static server like `http-server`:
```bash
npx http-server -c-1 -o
```
3. **Test the Application:**
- Open your browser and navigate to the URL provided by `http-server` (usually `http://localhost:8080`).
- Add a new service and verify it gets listed.
- View details of a service and add a review.
- Verify that the reviews are displayed correctly.
By following these steps and ensuring the code is correctly set up, the "Add Review" functionality works as expected.