Waver

Twitter-inspired social network

Links & Resources

Source Code

Check out the complete source code and documentation on GitHub.

View Repository
Live Demo

Experience the application in action through the live deployment.

Launch Application

Description

Microblogging social network inspired by Twitter, developed with Node.js, Express, Pug, and Bootstrap 5 for a dynamic and responsive social experience.

Waver is a platform that allows users to share concise thoughts, follow other users, and interact with a personalized news feed.

Main interface of Waver, showing the news feed and social interaction features.

Waver uses a server-side rendering stack for optimal performance, combining Node.js and Express for the backend, Pug as a template engine, and Bootstrap 5 for a responsive interface. Dynamic interactions are managed by client-side JavaScript and Socket.io for real-time notifications.

The application offers a complete range of social features:

  • User management with secure authentication
  • Publications (“Waves”) with text, images, and hashtags
  • Personalized news feed based on subscriptions
  • Private messaging between users

Several notable technical aspects have been implemented:

  • Protection against injections and XSS for enhanced security
  • Caching of frequent requests to optimize performance
  • Modular middlewares for a reusable server architecture
  • Pagination to efficiently handle large amounts of data

The application is deployed on Heroku with automated CI/CD via GitHub, MongoDB Atlas for the database, and Cloudinary for image storage.

// Extract from the posts controller
const Post = require('../models/postModel');
const User = require('../models/userModel');
const catchAsync = require('../utils/catchAsync');
const AppError = require('../utils/appError');

exports.createPost = catchAsync(async (req, res, next) => {
  // Extract hashtags from content
  const hashtagRegex = /#[a-zA-Z0-9_]+/g;
  const hashtags = req.body.content.match(hashtagRegex) || [];
  
  // Create the post
  const newPost = await Post.create({
    content: req.body.content,
    author: req.user._id,
    hashtags: hashtags.map(tag => tag.substring(1).toLowerCase()),
    image: req.file ? req.file.path : undefined
  });
  
  // Notify followers via Socket.io
  const followers = await User.find({ following: req.user._id });
  followers.forEach(follower => {
    if (req.io.sockets.connected[follower.socketId]) {
      req.io.sockets.connected[follower.socketId].emit('new-post', {
        authorName: req.user.username,
        authorId: req.user._id,
        postId: newPost._id
      });
    }
  });
  
  res.status(201).render('partials/post', {
    post: newPost,
    user: req.user,
    moment: require('moment')
  });
});

// Get news feed
exports.getFeed = catchAsync(async (req, res, next) => {
  const page = parseInt(req.query.page) || 1;
  const limit = 20;
  const skip = (page - 1) * limit;
  
  // MongoDB aggregation for personalized feed
  const posts = await Post.aggregate([
    {
      $match: {
        $or: [
          { author: { $in: [...req.user.following, req.user._id] } },
          { hashtags: { $in: req.user.interests || [] } }
        ]
      }
    },
    { $sort: { createdAt: -1 } },
    { $skip: skip },
    { $limit: limit },
    {
      $lookup: {
        from: 'users',
        localField: 'author',
        foreignField: '_id',
        as: 'authorDetails'
      }
    },
    { $unwind: '$authorDetails' }
  ]);
  
  res.status(200).render('feed', {
    title: 'Your Feed',
    posts,
    user: req.user,
    moment: require('moment'),
    currentPage: page,
    hasMore: posts.length === limit
  });
});