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:

  1. Project Setup:
mkdir RateMyTA
cd RateMyTA
npm init -y
npm install express ejs express-session body-parser bcrypt
  1. 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
  1. 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}`);
});
  1. 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
};
  1. 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>
  1. 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>
  1. public/js/main.js:
function updateTime() {
    const now = new Date();
    document.getElementById('current-time').textContent = now.toLocaleString();
}

setInterval(updateTime, 1000);
updateTime();
  1. 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>
  1. 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>
  1. 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>
  1. 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>
  1. 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>
  1. 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>
  1. 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');