Tut 10: RateMyTA Web Application
Create a Node.js and Express.js based web application called "RateMyTA" where students can rate and review their Teaching Assistants. The application should have the following features:
1. Reusable Templates:
Create reusable templates for the header and footer only. Header should include the site name, logo, and current date/time (updating every second). Footer should have links to "Terms of Service" and "Privacy Policy".
2. Server-side Data Management:
Manage two JSON files on the server:
- a. "users.json" for storing user credentials (username, hashed password, role)
- b. "ratings.json" for storing TA ratings and reviews
3. Pages and Functionality:
- Home page
- TA List page
- Rate a TA page (requires login)
- User Registration page
- User Login/Logout
- About Us page
4. Implement the following utility functions:
- A. calculateAverage(numbers)
- B. generateUniqueID()
- C. capitalizeWords(str)
- D. sortArrayByProperty(array, property, descending = false)
5. Form Handling:
- Create forms for user registration, login, and TA rating submission.
- Implement client-side and server-side validation.
6. Session Management:
Implement user sessions for logged-in users.
Solution Steps:
- Project Setup:
mkdir RateMyTA
cd RateMyTA
npm init -y
npm install express ejs express-session body-parser bcrypt
- File Structure:
You can create the following file structure automatically by running the create-structure.js code found in the appendix using the command:
node create-structure.js
Or create the following file structure manually:
RateMyTA/
├── server.js
├── views/
│ ├── partials/
│ │ ├── header.ejs
│ │ └── footer.ejs
│ ├── home.html
│ ├── ta-list.html
│ ├── rate-ta.html
│ ├── register.html
│ ├── login.html
│ └── about.html
├── public/
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── main.js
├── data/
│ ├── users.txt
│ └── ratings.txt
└── utils/
└── helpers.js
- server.js:
const express = require('express');
const session = require('express-session');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const bcrypt = require('bcrypt');
const ejs = require('ejs');
const { calculateAverage, generateUniqueID, capitalizeWords, sortArrayByProperty } = require('./utils/helpers');
const app = express();
const PORT = 3000;
const usersFile = path.join(__dirname, 'data', 'users.txt');
const ratingsFile = path.join(__dirname, 'data', 'ratings.txt');
app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
secret: 'ratemyta-secret',
resave: false,
saveUninitialized: true
}));
// Middleware to inject header and footer
app.use((req, res, next) => {
ejs.renderFile(path.join(__dirname, 'views', 'partials', 'header.ejs'), { user: req.session.user }, (err, header) => {
if (err) return res.status(500).send('Error loading header');
ejs.renderFile(path.join(__dirname, 'views', 'partials', 'footer.ejs'), (err, footer) => {
if (err) return res.status(500).send('Error loading footer');
res.locals.header = header;
res.locals.footer = footer;
next();
});
});
});
// Utility function to read data from text files
function readData(filePath, callback) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) return callback(err);
const parsedData = data.trim().split('\n').map(line => line.split(':'));
callback(null, parsedData);
});
}
// Utility function to write data to text files
function writeData(filePath, data, callback) {
const content = data.map(item => item.join(':')).join('\n');
fs.writeFile(filePath, content, 'utf8', callback);
}
// Routes
app.get('/', (req, res) => {
fs.readFile(path.join(__dirname, 'views', 'home.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading home page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.get('/ta-list', (req, res) => {
readData(ratingsFile, (err, ratingsData) => {
if (err) return res.status(500).send('Error reading ratings data');
const taAverages = {};
ratingsData.forEach(([reviewID, taName, rating, review, username]) => {
if (!taAverages[taName]) {
taAverages[taName] = [];
}
taAverages[taName].push(Number(rating));
});
const taList = Object.entries(taAverages).map(([taName, ratings]) => ({
taName: capitalizeWords(taName),
averageRating: calculateAverage(ratings)
}));
const sortedTAList = sortArrayByProperty(taList, 'averageRating', true);
fs.readFile(path.join(__dirname, 'views', 'ta-list.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading TA list page');
const renderedContent = ejs.render(content, { taList: sortedTAList });
res.send(res.locals.header + renderedContent + res.locals.footer);
});
});
});
app.get('/rate-ta', (req, res) => {
if (!req.session.user) {
return res.redirect('/login');
}
fs.readFile(path.join(__dirname, 'views', 'rate-ta.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading rate TA page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.post('/rate-ta', (req, res) => {
if (!req.session.user) {
return res.redirect('/login');
}
const { taName, rating, review } = req.body;
const reviewID = generateUniqueID();
readData(ratingsFile, (err, ratingsData) => {
if (err) return res.status(500).send('Error reading ratings data');
ratingsData.push([reviewID, taName.toLowerCase(), rating, review, req.session.user]);
writeData(ratingsFile, ratingsData, (err) => {
if (err) return res.status(500).send('Error writing ratings data');
res.redirect('/ta-list');
});
});
});
app.get('/register', (req, res) => {
fs.readFile(path.join(__dirname, 'views', 'register.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading registration page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.post('/register', (req, res) => {
const { username, password } = req.body;
const hashedPassword = bcrypt.hashSync(password, 10);
fs.readFile(usersFile, 'utf8', (err, data) => {
if (err) return res.status(500).send('Server error.');
if (data.split('\n').some(line => line.split(':')[0] === username)) {
return res.send('Username already exists.');
}
fs.appendFile(usersFile, `${username}:${hashedPassword}\n`, (err) => {
if (err) return res.status(500).send('Server error.');
res.send('Account created successfully.');
});
});
});
app.get('/login', (req, res) => {
fs.readFile(path.join(__dirname, 'views', 'login.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading login page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.post('/login', (req, res) => {
const { username, password } = req.body;
readData(usersFile, (err, userData) => {
if (err) return res.status(500).send('Server error.');
const user = userData.find(u => u[0] === username);
if (user && bcrypt.compareSync(password, user[1])) {
req.session.user = username;
res.redirect('/');
} else {
res.redirect('/login');
}
});
});
app.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/');
});
app.get('/about', (req, res) => {
fs.readFile(path.join(__dirname, 'views', 'about.html'), 'utf8', (err, content) => {
if (err) return res.status(500).send('Error loading about page');
res.send(res.locals.header + content + res.locals.footer);
});
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
- utils/helpers.js:
function calculateAverage(numbers) {
if (numbers.length === 0) return 0;
const sum = numbers.reduce((acc, num) => acc + num, 0);
return (sum / numbers.length).toFixed(2);
}
function generateUniqueID() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 5);
}
function capitalizeWords(str) {
return str.replace(/\b\w/g, l => l.toUpperCase());
}
function sortArrayByProperty(array, property, descending = false) {
return array.sort((a, b) => descending ? b[property] - a[property] : a[property] - b[property]);
}
module.exports = {
calculateAverage,
generateUniqueID,
capitalizeWords,
sortArrayByProperty
};
- views/partials/header.ejs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RateMyTA</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>RateMyTA</h1>
<nav>
<a href="/">Home</a>
<a href="/ta-list">TA List</a>
<% if (user) { %>
<a href="/rate-ta">Rate a TA</a>
<a href="/logout">Logout</a>
<% } else { %>
<a href="/register">Register</a>
<a href="/login">Login</a>
<% } %>
<a href="/about">About Us</a>
</nav>
<div id="current-time"></div>
</header>
<main>
- views/partials/footer.ejs:
</main>
<footer>
<a href="/terms">Terms of Service</a>
<a href="/privacy">Privacy Policy</a>
</footer>
<script src="/js/main.js"></script>
</body>
</html>
- public/js/main.js:
function updateTime() {
const now = new Date();
document.getElementById('current-time').textContent = now.toLocaleString();
}
setInterval(updateTime, 1000);
updateTime();
- views/about.html:
<h2>About RateMyTA</h2>
<p>RateMyTA is a platform designed to help students share their experiences with Teaching Assistants and make informed decisions about their education.</p>
<p>Our mission is to promote transparency and improve the quality of education by providing a space for constructive feedback and recognition of outstanding TAs.</p>
<p>We believe that by fostering open communication between students and TAs, we can create a better learning environment for everyone.</p>
- views/home.html:
<h2>Welcome to RateMyTA</h2>
<p>RateMyTA is a platform where students can rate and review their Teaching Assistants. Help your fellow students by sharing your experiences and find out which TAs are highly recommended by others.</p>
<p>To get started, check out our <a href="/ta-list">TA List</a> or <a href="/register">create an account</a> to submit your own ratings.</p>
- views/login.html:
<h2>Login</h2>
<form action="/login" method="POST">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Login</button>
</form>
<p>Don't have an account? <a href="/register">Register here</a></p>
- views/rate-ta.html:
<h2>Rate a TA</h2>
<form action="/rate-ta" method="POST">
<label for="taName">TA Name:</label>
<input type="text" id="taName" name="taName" required>
<label for="rating">Rating (1-5):</label>
<select id="rating" name="rating" required>
<option value="1">1 - Poor</option>
<option value="2">2 - Fair</option>
<option value="3">3 - Good</option>
<option value="4">4 - Very Good</option>
<option value="5">5 - Excellent</option>
</select>
<label for="review">Review:</label>
<textarea id="review" name="review" rows="4" required></textarea>
<button type="submit">Submit Rating</button>
</form>
- views/register.html:
<h2>Register</h2>
<form action="/register" method="POST">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<button type="submit">Register</button>
</form>
<p>Already have an account? <a href="/login">Login here</a></p>
- views/ta-list.html:
<h2>TA List</h2>
<ul class="ta-list">
<% taList.forEach(ta => { %>
<li class="ta-item">
<span class="ta-name"><%= ta.taName %></span>
<span class="ta-rating">Average Rating: <%= ta.averageRating %></span>
</li>
<% }) %>
</ul>
- public/css/style.css:
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--background-color: #ecf0f1;
--text-color: #34495e;
}
body {
font-family: 'Arial', sans-serif;
line-height: 1.6;
color: var(--text-color);
background-color: var(--background-color);
margin: 0;
padding: 0;
}
header {
background-color: var(--primary-color);
color: white;
text-align: center;
padding: 1rem;
}
header h1 {
margin: 0;
}
nav {
display: flex;
justify-content: center;
margin-top: 1rem;
}
nav a {
color: white;
text-decoration: none;
padding: 0.5rem 1rem;
margin: 0 0.5rem;
border-radius: 5px;
transition: background-color 0.3s;
}
nav a:hover {
background-color: rgba(255, 255, 255, 0.2);
}
main {
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
h2 {
color: var(--primary-color);
}
form {
background-color: white;
padding: 2rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
input[type="text"],
input[type="password"],
textarea,
select {
width: 100%;
padding: 0.5rem;
margin-bottom: 1rem;
border: 1px solid #ddd;
border-radius: 3px;
}
button {
background-color: var(--secondary-color);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 3px;
cursor: pointer;
transition: background-color 0.3s;
}
button:hover {
background-color: #27ae60;
}
.ta-list {
list-style-type: none;
padding: 0;
}
.ta-item {
background-color: white;
margin-bottom: 1rem;
padding: 1rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.ta-name {
font-weight: bold;
color: var(--primary-color);
}
.ta-rating {
color: var(--secondary-color);
}
footer {
background-color: var(--primary-color);
color: white;
text-align: center;
padding: 1rem;
position: fixed;
bottom: 0;
width: 100%;
}
footer a {
color: white;
text-decoration: none;
margin: 0 0.5rem;
}
#current-time {
font-size: 0.9rem;
margin-top: 0.5rem;
}
Appendix:
- create-structure.js:
const fs = require('fs');
const path = require('path');
// Define the directory structure
const structure = {
'server.js': '',
'views': {
'partials': {
'header.ejs': '',
'footer.ejs': ''
},
'home.html': '',
'ta-list.html': '',
'rate-ta.html': '',
'register.html': '',
'login.html': '',
'about.html': ''
},
'public': {
'css': {
'style.css': ''
},
'js': {
'main.js': ''
}
},
'data': {
'users.txt': '',
'ratings.txt': ''
},
'utils': {
'helpers.js': ''
}
};
// Function to create directories and files recursively
function createStructure(base, struct) {
for (const key in struct) {
const fullPath = path.join(base, key);
if (typeof struct[key] === 'string') {
fs.writeFileSync(fullPath, struct[key]);
} else {
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath);
}
createStructure(fullPath, struct[key]);
}
}
}
// Create the directory structure inside the existing RateMyTA directory
createStructure('RateMyTA', structure);
console.log('Directory structure created successfully');