├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── README.md ├── config └── connection.js ├── controllers ├── api │ ├── index.js │ ├── message-routes.js │ └── user-routes.js ├── home-routes.js └── index.js ├── db └── schema.sql ├── models ├── Message.js ├── User.js └── index.js ├── package-lock.json ├── package.json ├── public └── assets │ ├── css │ └── style.css │ ├── images │ ├── logo-dark.png │ ├── logo-dark.svg │ ├── logo-light.png │ ├── logo-light.svg │ └── randm-icon.png │ └── js │ ├── delete-user.js │ ├── login.js │ ├── logout.js │ ├── message.js │ ├── random-chat.js │ ├── recent.js │ ├── register.js │ └── socket.js ├── seeds ├── index.js ├── message-seeds.js └── user-seeds.js ├── server.js ├── utils ├── filters.js └── helpers.js └── views ├── chat.handlebars ├── layouts └── main.handlebars ├── login.handlebars └── register.handlebars /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "node": true, 7 | "es6": true, 8 | "es2017": true 9 | }, 10 | "rules": { 11 | "no-undef-init": "error", 12 | "no-duplicate-case": "error", 13 | "no-empty": "error", 14 | "no-extra-semi": "error", 15 | "no-func-assign": "error", 16 | "no-irregular-whitespace": "error", 17 | "no-unreachable": "error", 18 | "curly": "error", 19 | "dot-notation": "error", 20 | "eqeqeq": "error", 21 | "no-empty-function": "error", 22 | "no-multi-spaces": "error", 23 | "no-mixed-spaces-and-tabs": "error", 24 | "no-trailing-spaces": "error", 25 | "default-case": "error", 26 | "no-fallthrough": "error", 27 | "no-unused-vars": "error", 28 | "no-use-before-define": "error", 29 | "no-redeclare": "error", 30 | "brace-style": "error", 31 | "indent": ["error", 2], 32 | "quotes": ["error", "single"], 33 | "semi": ["error", "always"], 34 | "radix": "off" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .env 4 | frontend-tests-NOT-PUBLIC/ -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "bracketSameLine": true 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RANDM - The Random Dating App 2 | 3 | **RANDM** is a dating application that helps users find matches through randomization. While other dating apps on the market focus too much on matches based on initial judgement, with RANDM, people can start chatting with and getting to know potential dates without that initial judgement in an app free of profile pictures and “swiping”. 4 | 5 | ## Table of Contents 6 | 7 | * [User Story](#user-story) 8 | * [Screenshot](#screenshot) 9 | * [Live Deployment](#live-deployment) 10 | * [Created With](#created-with) 11 | * [Installation and Usage](#installation-and-usage) 12 | * [Contributing](#contributing) 13 | * [Tests](#tests) 14 | * [Questions](#questions) 15 | 16 | ## User Story 17 | 18 | ``` 19 | AS A person who has trouble finding dating matches 20 | I WANT to use a dating app that randomly matches users 21 | SO THAT when I click on the RANDM button, a new page opens with a random potential date with whom I can start chatting. 22 | ``` 23 | 24 | ``` 25 | WHEN I open the app 26 | THEN I am presented with the title of the app and the login form. 27 | WHEN I choose to register 28 | THEN I'm presented with inputting my first name, last name, email, password, gender identity, sexual preferences, pronouns, birthday, and bio. 29 | WHEN I click on login 30 | THEN I'm presented with entering my email and password. 31 | WHEN I click the randomize a new chat button 32 | THEN I'm presented with a new open chat with a random user. 33 | WHEN I click on logout 34 | THEN I'm presented with the homepage screen. 35 | ``` 36 | 37 | ## Screenshot 38 | 39 | [![random](https://raw.githubusercontent.com/JColeCodes/cc-portfolio/main/assets/images/portfolio/Randm.jpg)](https://ran-dm.herokuapp.com/) 40 | 41 | ## Live Deployment 42 | 43 | This application is deployed using Heroku: 44 | 45 | - [Ran-dm.Herokuapp.com](https://ran-dm.herokuapp.com/) 46 | 47 | ## Created With 48 | 49 | * Node.js + Express.js 50 | * MySQL / MySQL2 51 | * Sequelize 52 | * Handlebars 53 | * Bcrypt 54 | * Socket.io 55 | 56 | ![JavaScript Badge](https://img.shields.io/badge/-JavaScript-yellow?style=for-the-badge&logo=appveyor) 57 | ![CSS Badge](https://img.shields.io/badge/-CSS-blueviolet?style=for-the-badge&logo=appveyor) 58 | ![Handlebars](https://img.shields.io/badge/-Handlebars-orange?style=for-the-badge&logo=appveyor) 59 | 60 | ## Installation and Usage 61 | 62 | To install and run this project, please follow these steps: 63 | 1. Make sure you have [Node.js](https://nodejs.org) and [MySQL](https://dev.mysql.com/downloads/) installed. 64 | 2. Through the command line, go to the folder you wish this application's folder to be in. 65 | 3. Do `git clone` of the repository to get the application's files. 66 | 4. Run `npm run schema` to get the database. 67 | 5. To install all of the depenencies this application uses, run `npm install`. 68 | 6. Create a `.env` file containing: `DB_NAME=randm_db`, along wtih your `DB_USER`, `DB_PASSWORD`, and a secret code `SECRET_SECRET`. 69 | 7. To start the application, run `npm start`. 70 | 8. Open [localhost:3001](http://localhost:3001/) to see the local webpage. 71 | 72 | ## Contributing 73 | 74 | RANDM is a work in progress! If you would like to contribute to this project, you can do so by: 75 | 1. Forking the project. ([Learn how to fork.](https://docs.github.com/en/get-started/quickstart/fork-a-repo)) 76 | 2. Creating a new feature branch, committing the changes, and pushing the branch. 77 | 3. Opening a [Pull Request](https://github.com/JColeCodes/randm/pulls). 78 | 79 | Read the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). 80 | 81 | ## Tests 82 | 83 | To easily test the application with a few users already registered, you can seed the database by running the following command: 84 | ``` 85 | npm run seed 86 | ``` 87 | 88 | ## Questions 89 | RANDM was created by Jennifer Cole, Lex Slovik, Charlie Hua, Chuong Vo, Marielle Champagne, Ahmad Anees, Gavin Jacobsen, Rex Oliver. 90 | 91 | For inquiries regarding the project, please email Jennifer Cole at [capauldi@gmail.com](mailto:capauldi@gmail.com). 92 | -------------------------------------------------------------------------------- /config/connection.js: -------------------------------------------------------------------------------- 1 | // Import Sequelize 2 | const Sequelize = require('sequelize'); 3 | // Require .env for hiding database information 4 | require('dotenv').config(); 5 | 6 | let sequelize; 7 | 8 | // If connected to JawsDB 9 | if (process.env.JAWSDB_URL) { 10 | sequelize = new Sequelize(process.env.JAWSDB_URL); 11 | } else if (process.env.MYSQL_URL) { 12 | sequelize = new Sequelize(process.env.MYSQL_URL); 13 | } else { 14 | // Else, create connection to our local database 15 | sequelize = new Sequelize( 16 | process.env.DB_NAME, 17 | process.env.DB_USER, 18 | process.env.DB_PASSWORD, 19 | { 20 | host: 'localhost', 21 | dialect: 'mysql', 22 | port: 3306 23 | } 24 | ); 25 | } 26 | 27 | module.exports = sequelize; 28 | -------------------------------------------------------------------------------- /controllers/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | const userRoutes = require('./user-routes'); 4 | const messageRoutes = require('./message-routes'); 5 | 6 | router.use('/users', userRoutes); 7 | router.use('/messages', messageRoutes); 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /controllers/api/message-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { User, Message } = require('../../models'); 3 | const { Op } = require('sequelize'); 4 | const { getUserLatest } = require('../../utils/filters'); 5 | 6 | // Get all messages of user if logged in 7 | router.get('/', (req, res) => { 8 | // If not logged in, redirect to login page 9 | if (!req.session.loggedIn) { 10 | res.redirect('/login'); 11 | return; 12 | } 13 | var sessionId = req.session.user_id; 14 | Message.findAll({ 15 | where: { 16 | // If receiver id OR sender id = session id 17 | [Op.or]: [{ receiver_id: sessionId }, { sender_id: sessionId }] 18 | }, 19 | attributes: ['id', 'message_text', 'createdAt'], 20 | include: [ 21 | { 22 | // Includes message sender 23 | model: User, 24 | as: 'sender', 25 | attributes: ['id', 'first_name', 'last_name'] 26 | }, 27 | { 28 | // Includes message receiver 29 | model: User, 30 | as: 'receiver', 31 | attributes: ['id', 'first_name', 'last_name'] 32 | } 33 | ] 34 | }) 35 | .then((dbMessageData) => res.json(dbMessageData)) 36 | .catch((err) => { 37 | console.log(err); 38 | res.status(500).json(err); 39 | }); 40 | }); 41 | 42 | // Get recent messages 43 | router.get('/recent', (req, res) => { 44 | // If not logged in, redirect to login page 45 | if (!req.session.loggedIn) { 46 | res.redirect('/login'); 47 | return; 48 | } 49 | var sessionId = req.session.user_id; 50 | Message.findAll({ 51 | where: { 52 | // If receiver id OR sender id = session id 53 | [Op.or]: [{ receiver_id: sessionId }, { sender_id: sessionId }] 54 | }, 55 | order: [['createdAt', 'DESC']] 56 | }) 57 | .then((dbMessageData) => { 58 | // Find all users 59 | User.findAll({ 60 | attributes: ['id', 'first_name', 'last_name'] 61 | }) 62 | .then((dbUserData) => { 63 | // Map users for plain javascript of data 64 | const user = dbUserData.map((user) => user.get({ plain: true })); 65 | 66 | // Map messages for plain javascript of data 67 | const messages = dbMessageData.map((message) => 68 | message.get({ plain: true }) 69 | ); 70 | 71 | // Gets latest chat message for every conversation user has 72 | let userLatest = getUserLatest(messages, user, sessionId); 73 | 74 | res.json(userLatest.latestChat); 75 | }) 76 | // Error catch for User.findAll 77 | .catch((err) => { 78 | console.log(err); 79 | res.status(500).json(err); 80 | }); 81 | }) 82 | // Error catch for Message.findAll 83 | .catch((err) => { 84 | console.log(err); 85 | res.status(500).json(err); 86 | }); 87 | }); 88 | 89 | // Post new message 90 | router.post('/', (req, res) => { 91 | // Creates new message with information sent from public/assets/message.js 92 | Message.create({ 93 | sender_id: req.body.sender_id, 94 | receiver_id: req.body.receiver_id, 95 | message_text: req.body.message_text 96 | }) 97 | .then((dbMessageData) => res.json(dbMessageData)) 98 | .catch((err) => { 99 | console.log(err); 100 | res.status(400).json(err); 101 | }); 102 | }); 103 | 104 | module.exports = router; -------------------------------------------------------------------------------- /controllers/api/user-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { User, Message } = require('../../models'); 3 | 4 | // Get all users 5 | router.get('/', (req, res) => { 6 | User.findAll({ 7 | attributes: { exclude: ['password', 'email', 'birthday'] } 8 | }) 9 | .then((dbUserData) => res.json(dbUserData)) 10 | .catch((err) => { 11 | console.log(err); 12 | res.status(500).json(err); 13 | }); 14 | }); 15 | 16 | // Find one user by id 17 | router.get('/:id', (req, res) => { 18 | User.findOne({ 19 | where: { id: req.params.id }, 20 | attributes: { exclude: ['password', 'email', 'birthday'] } 21 | }) 22 | .then((dbUserData) => { 23 | if (!dbUserData) { 24 | res.status(404).json({ message: 'No user found with this id' }); 25 | return; 26 | } 27 | res.json(dbUserData); 28 | }) 29 | .catch((err) => { 30 | console.log(err); 31 | res.status(500).json(err); 32 | }); 33 | }); 34 | 35 | // Add user to database on register 36 | router.post('/', (req, res) => { 37 | /* Expects: { 38 | "email": "myemail@email.com", 39 | "password": "password1234", 40 | "first_name": "user", 41 | "last_name": "name", 42 | "bio": "I am really interesting", 43 | "gender": "non-binary", 44 | "sexual_preference": "pansexual", 45 | "pronouns": "they/them", 46 | "birthday": "03/03/2003" 47 | } */ 48 | User.create(req.body) 49 | .then((dbUserData) => { 50 | res.json(dbUserData); 51 | }) 52 | .catch((err) => { 53 | console.log(err); 54 | res.status(500).json(err); 55 | }); 56 | }); 57 | 58 | // Login verification post route to /login endpoint 59 | router.post('/login', (req, res) => { 60 | // Expects: {"email": "myemail@email.com", "password": "password1234"} 61 | User.findOne({ 62 | where: { email: req.body.email } 63 | }).then((dbUserData) => { 64 | if (!dbUserData) { 65 | res.status(400).json({ message: 'No user found with that email address' }); 66 | return; 67 | } 68 | 69 | // Check password validity 70 | const validPassword = dbUserData.checkPassword(req.body.password); 71 | if (!validPassword) { 72 | res.status(400).json({ message: 'Incorrect password' }); 73 | return; 74 | } 75 | // Save session data and set status to loggedIn true 76 | req.session.save(() => { 77 | req.session.user_id = dbUserData.id; 78 | req.session.email = dbUserData.email; 79 | req.session.loggedIn = true; 80 | 81 | res.json({ user: dbUserData, message: 'Login successful' }); 82 | }); 83 | }); 84 | }); 85 | 86 | // Destroy the session on logout 87 | router.post('/logout', (req, res) => { 88 | if (req.session.loggedIn) { 89 | req.session.destroy(() => { 90 | res.status(204).end(); 91 | }); 92 | } else { 93 | res.status(404).end(); 94 | } 95 | }); 96 | 97 | // Delete a user by id 98 | router.delete('/:id', (req, res) => { 99 | User.destroy({ 100 | where: { 101 | id: req.params.id 102 | } 103 | }) 104 | .then((dbUserData) => { 105 | if (!dbUserData) { 106 | res.status(404).json({ message: 'No user found with this id' }); 107 | return; 108 | } 109 | res.json(dbUserData); 110 | }) 111 | .catch((err) => { 112 | console.log(err); 113 | res.status(500).json(err); 114 | }); 115 | }); 116 | 117 | module.exports = router; -------------------------------------------------------------------------------- /controllers/home-routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const { User, Message } = require('../models'); 3 | const { getUserLatest } = require('../utils/filters'); 4 | const { Op } = require('sequelize'); 5 | 6 | var sessionId; 7 | 8 | // Redirect homepage to either login page (if not logged in) OR chat page (if logged in) 9 | router.get('/', (req, res) => { 10 | if (!req.session.loggedIn) { 11 | res.redirect('/login'); 12 | return; 13 | } else { 14 | res.redirect('/chat'); 15 | return; 16 | } 17 | }); 18 | 19 | // Show login page if user is logged out, but show /chat page if user is logged in 20 | router.get('/login', (req, res) => { 21 | if (req.session.loggedIn) { 22 | res.redirect('/chat'); 23 | return; 24 | } 25 | res.render('login'); 26 | }); 27 | 28 | // Show register page if user is logged out, but show /chat page if user is logged in 29 | router.get('/register', (req, res) => { 30 | if (req.session.loggedIn) { 31 | res.redirect('/chat'); 32 | return; 33 | } 34 | res.render('register'); 35 | }); 36 | 37 | // Finds information for currently logged in user to display information properly 38 | router.get('/chat', (req, res) => { 39 | // Redirect to /login if not logged in 40 | if (!req.session.loggedIn) { 41 | res.redirect('/login'); 42 | return; 43 | } 44 | 45 | sessionId = req.session.user_id; // Get session user's id 46 | User.findAll({ 47 | attributes: ['id', 'first_name', 'last_name'] 48 | }) 49 | .then((dbUserData) => { 50 | // Map users for plain javascript of data 51 | const user = dbUserData.map((user) => user.get({ plain: true })); 52 | 53 | // Gets information for current user 54 | const userLatest = getUserLatest(null, user, sessionId); 55 | 56 | // Renders page with chat handlebars 57 | res.render('chat', { 58 | userLatest, 59 | loggedIn: req.session.loggedIn, 60 | chatHome: true // Boolean for if chat is page home or user page 61 | }); 62 | }) 63 | // Error catch for User.findAll 64 | .catch((err) => { 65 | console.log(err); 66 | res.status(500).json(err); 67 | }); 68 | }); 69 | 70 | // Find all messages between two specific users 71 | router.get('/chat/:id', (req, res) => { 72 | // Redirect to /login if not logged in 73 | if (!req.session.loggedIn) { 74 | res.redirect('/login'); 75 | return; 76 | } 77 | // If user is chatting with everyone, reroute undefined to chat home 78 | if (req.params.id == 'undefined') { 79 | res.redirect('/chat'); 80 | return; 81 | } 82 | 83 | sessionId = req.session.user_id; 84 | // If user goes to page with their own id, redirect them to home, they can't chat alone 85 | if (req.params.id == sessionId) { 86 | res.redirect('/chat'); 87 | return; 88 | } 89 | Message.findAll({ 90 | where: { 91 | [Op.or]: [ 92 | // If receiver id = session id AND sender id = param id 93 | { receiver_id: sessionId, sender_id: req.params.id }, 94 | // OR receiver id = param id AND sender id = session id 95 | { receiver_id: req.params.id, sender_id: sessionId } 96 | ] 97 | }, 98 | // Display all messages in order by message id 99 | order: [['id']] 100 | }) 101 | .then((dbMessageData) => { 102 | // Gets all users and info 103 | User.findAll({ 104 | attributes: [ 105 | 'id', 106 | 'first_name', 107 | 'last_name', 108 | 'pronouns', 109 | 'gender', 110 | 'sexual_preference', 111 | 'bio' 112 | ] 113 | }) 114 | .then((dbUserData) => { 115 | // Map users for plain javascript of data 116 | const user = dbUserData.map((user) => user.get({ plain: true })); 117 | 118 | // Searches to see if the param id exists in the user table 119 | let userExist = false; 120 | user.forEach((user) => { 121 | if (user.id == req.params.id) { 122 | userExist = true; 123 | return; 124 | } 125 | }); 126 | // If user does not exist, redirect to chat home 127 | if (!userExist) { 128 | res.redirect('/'); 129 | return; 130 | } 131 | 132 | // Map messages for plain javascript of data 133 | const messages = dbMessageData.map((message) => 134 | message.get({ plain: true }) 135 | ); 136 | 137 | // Gets information for current user 138 | const userLatest = getUserLatest( 139 | messages, 140 | user, 141 | sessionId, 142 | req.params.id 143 | ); 144 | 145 | // Render all messages on specific chat page 146 | res.render('chat', { 147 | messages, 148 | userLatest, 149 | loggedIn: req.session.loggedIn, 150 | chatHome: false // Boolean for if chat is page home or user page 151 | }); 152 | }) 153 | // Error catch for User.findAll 154 | .catch((err) => { 155 | console.log(err); 156 | res.status(500).json(err); 157 | }); 158 | }) 159 | // Error catch for Message.findAll 160 | .catch((err) => { 161 | console.log(err); 162 | res.status(500).json(err); 163 | }); 164 | }); 165 | 166 | // Redirect to homepage for any page that does not exist 167 | router.get('*', (req, res) => { 168 | res.redirect('/'); 169 | }); 170 | 171 | module.exports = router; -------------------------------------------------------------------------------- /controllers/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const apiRoutes = require('./api'); 3 | const homeRoutes = require('./home-routes.js'); 4 | 5 | router.use('/api', apiRoutes); // Routes for /api 6 | router.use('/', homeRoutes); // Routes for public files and pages 7 | 8 | router.use((req, res) => { 9 | res.status(404).end(); 10 | }); 11 | 12 | module.exports = router; -------------------------------------------------------------------------------- /db/schema.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS randm_db; 2 | 3 | CREATE DATABASE randm_db; -------------------------------------------------------------------------------- /models/Message.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes } = require('sequelize'); 2 | const sequelize = require('../config/connection'); 3 | 4 | class Message extends Model {} 5 | 6 | Message.init( 7 | { 8 | id: { 9 | type: DataTypes.INTEGER, 10 | allowNull: false, 11 | primaryKey: true, 12 | autoIncrement: true 13 | }, 14 | sender_id: { 15 | type: DataTypes.INTEGER, 16 | allowNull: false, 17 | references: { 18 | model: 'user', 19 | key: 'id' 20 | } 21 | }, 22 | receiver_id: { 23 | type: DataTypes.INTEGER, 24 | allowNull: false, 25 | references: { 26 | model: 'user', 27 | key: 'id' 28 | } 29 | }, 30 | message_text: { 31 | type: DataTypes.STRING, 32 | allowNull: false 33 | } 34 | }, 35 | { 36 | sequelize, 37 | freezeTableName: true, 38 | underscored: true, 39 | modelName: 'message' 40 | } 41 | ); 42 | 43 | module.exports = Message; -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const { Model, DataTypes } = require('sequelize'); 2 | const sequelize = require('../config/connection'); 3 | const bcrypt = require('bcrypt'); 4 | 5 | class User extends Model { 6 | checkPassword(loginPW) { 7 | return bcrypt.compareSync(loginPW, this.password); 8 | } 9 | } 10 | 11 | User.init( 12 | { 13 | id: { 14 | type: DataTypes.INTEGER, 15 | allowNull: false, 16 | primaryKey: true, 17 | autoIncrement: true 18 | }, 19 | first_name: { 20 | type: DataTypes.STRING, 21 | allowNUll: false 22 | }, 23 | last_name: { 24 | type: DataTypes.STRING, 25 | allowNUll: false 26 | }, 27 | email: { 28 | type: DataTypes.STRING, 29 | allowNull: false, 30 | unique: true, 31 | validate: { 32 | isEmail: true 33 | } 34 | }, 35 | password: { 36 | type: DataTypes.STRING, 37 | allowNull: false, 38 | validate: { 39 | len: [8] 40 | } 41 | }, 42 | bio: { 43 | type: DataTypes.STRING, 44 | allowNull: true 45 | }, 46 | gender: { 47 | type: DataTypes.STRING, 48 | allowNull: false 49 | }, 50 | sexual_preference: { 51 | type: DataTypes.STRING, 52 | allowNull: false 53 | }, 54 | pronouns: { 55 | type: DataTypes.STRING, 56 | allowNull: true 57 | }, 58 | birthday: { 59 | type: DataTypes.STRING, 60 | allowNull: false 61 | } 62 | }, 63 | { 64 | hooks: { 65 | async beforeCreate(newUserData) { 66 | newUserData.password = await bcrypt.hash(newUserData.password, 10); 67 | return newUserData; 68 | }, 69 | async beforeUpdate(updatedUserData) { 70 | updatedUserData.password = await bcrypt.hash( 71 | updatedUserData.password, 72 | 10 73 | ); 74 | return updatedUserData; 75 | } 76 | }, 77 | sequelize, 78 | timestamps: false, 79 | freezeTableName: true, 80 | underscored: true, 81 | modelName: 'user' 82 | } 83 | ); 84 | 85 | module.exports = User; -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | const User = require('./User'); 2 | const Message = require('./Message'); 3 | 4 | User.hasMany(Message, { 5 | foreignKey: 'sender_id', 6 | as: 'sender' 7 | }); 8 | Message.belongsTo(User, { 9 | foreignKey: 'sender_id', 10 | as: 'sender' 11 | }); 12 | 13 | User.hasMany(Message, { 14 | foreignKey: 'receiver_id', 15 | as: 'receiver' 16 | }); 17 | Message.belongsTo(User, { 18 | foreignKey: 'receiver_id', 19 | as: 'receiver' 20 | }); 21 | 22 | module.exports = { User, Message }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "randm", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js", 9 | "watch": "nodemon server", 10 | "schema": "mysql -u root -p < db/schema.sql", 11 | "seed": "node seeds/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/JColeCodes/randm.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/JColeCodes/randm/issues" 21 | }, 22 | "homepage": "https://github.com/JColeCodes/randm#readme", 23 | "dependencies": { 24 | "bcrypt": "^5.0.1", 25 | "connect-session-sequelize": "^7.1.2", 26 | "dotenv": "^16.0.0", 27 | "eslint": "^8.8.0", 28 | "eslint-config-prettier": "^8.3.0", 29 | "express": "^4.17.2", 30 | "express-handlebars": "^6.0.2", 31 | "express-session": "^1.17.2", 32 | "mysql2": "^2.3.3", 33 | "prettier": "^2.5.1", 34 | "sequelize": "^6.16.0", 35 | "socket.io": "^4.4.1" 36 | }, 37 | "devDependencies": { 38 | "@types/bcrypt": "^5.0.0", 39 | "@types/express-session": "^1.17.4", 40 | "nodemon": "^2.0.15" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/assets/css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-color: #59276d; 3 | /* Main Color */ 4 | --secondary-color: #bc34ae; 5 | /* Secondary Color */ 6 | --secondary-lighter: #ed67df; 7 | /* Lighter Version of Secondary Color */ 8 | --lightest-color: #f6f9f9; 9 | /* Light-Gray Color */ 10 | --mid-color: #afb5b6; 11 | /* Mid-Gray Color */ 12 | --darkest-color: #7c8385; 13 | /* Darker-Gray Color */ 14 | --sidebar-color: #edf3f3; 15 | /* Sidebar Background Color */ 16 | --sidebar-highlight-color: #e0e9e9; 17 | /* Sidebar Highlight Color */ 18 | } 19 | 20 | * { 21 | margin: 0; 22 | padding: 0; 23 | box-sizing: border-box; 24 | } 25 | 26 | body { 27 | font-family: 'Lato', sans-serif; 28 | font-size: 16px; 29 | line-height: 1; 30 | background-color: var(--lightest-color); 31 | color: var(--darkest-color); 32 | } 33 | 34 | a, 35 | a:hover, 36 | button, 37 | button:hover, 38 | li, 39 | li:hover { 40 | transition: all 0.5s ease-in-out; 41 | } 42 | 43 | button:hover { 44 | cursor: pointer; 45 | } 46 | 47 | a { 48 | color: var(--main-color); 49 | text-decoration: none; 50 | } 51 | 52 | a:hover { 53 | color: var(--secondary-color); 54 | } 55 | 56 | input:focus { 57 | outline: 0; 58 | } 59 | 60 | .open-toggle, 61 | .close-toggle { 62 | display: none; 63 | } 64 | 65 | 66 | /* IF NOT LOGGED IN */ 67 | 68 | .unlogged-page { 69 | display: flex; 70 | flex-wrap: wrap; 71 | justify-content: space-between; 72 | align-items: center; 73 | justify-content: center; 74 | padding: 20px; 75 | min-height: calc(100vh - 40px); 76 | } 77 | 78 | 79 | /* Display logo on left */ 80 | 81 | .unlogged-page .logo { 82 | width: calc(40% - 30px); 83 | margin: 0 100px 0 20px; 84 | } 85 | 86 | .unlogged-page .logo img { 87 | width: 100%; 88 | } 89 | 90 | 91 | /* Display form on right */ 92 | 93 | .unlogged-page .forms { 94 | width: calc(40% - 40px); 95 | margin: 0; 96 | background-color: var(--sidebar-color); 97 | padding: 20px; 98 | font-size: 1em; 99 | } 100 | 101 | .unlogged-page .forms input, 102 | .unlogged-page .forms select, 103 | .unlogged-page .forms textarea { 104 | padding: 10px; 105 | margin: 5px 0 15px; 106 | width: 100%; 107 | font-size: 1em; 108 | background-color: white; 109 | border: 1px solid var(--sidebar-highlight-color); 110 | color: var(--darkest-color); 111 | font-family: 'Lato', sans-serif; 112 | } 113 | 114 | .unlogged-page .forms textarea { 115 | height: 120px; 116 | } 117 | 118 | .unlogged-page .forms input:focus, 119 | .unlogged-page .forms select:focus, 120 | .unlogged-page .forms textarea:focus { 121 | outline: 1px solid var(--secondary-lighter); 122 | border: 1px solid white; 123 | } 124 | 125 | .unlogged-page .forms button { 126 | padding: 10px; 127 | font-size: 1em; 128 | border: none; 129 | background-color: var(--secondary-lighter); 130 | color: white; 131 | } 132 | 133 | .unlogged-page .forms .form-message { 134 | font-size: 0.9em; 135 | font-style: italic; 136 | margin: 15px 0 0; 137 | } 138 | 139 | 140 | /* IF USER IS LOGGED IN */ 141 | 142 | .logged-in { 143 | display: flex; 144 | justify-content: space-between; 145 | flex-flow: column; 146 | height: 100vh; 147 | } 148 | 149 | 150 | /* Header */ 151 | 152 | .logged-in header { 153 | display: flex; 154 | flex-wrap: wrap; 155 | justify-content: space-between; 156 | align-items: center; 157 | padding: 0 20px; 158 | background-color: var(--main-color); 159 | color: white; 160 | } 161 | 162 | .logged-in header .logo { 163 | width: 10%; 164 | } 165 | 166 | .logged-in header .logo img { 167 | height: 60px; 168 | margin: 10px 0; 169 | } 170 | 171 | .logged-in header .welcome-info i { 172 | font-size: 1.5em; 173 | padding-bottom: 10px; 174 | margin-top: 10px; 175 | } 176 | 177 | .logged-in header .welcome-info .user-options { 178 | z-index: -1; 179 | position: absolute; 180 | background-color: white; 181 | border: 1px solid var(--sidebar-highlight-color); 182 | box-shadow: 0 0 1px var(--darkest-color); 183 | padding: 10px; 184 | right: 20px; 185 | opacity: 0; 186 | transition: all 0.1s ease-in-out; 187 | } 188 | 189 | .logged-in header .welcome-info:hover .user-options { 190 | z-index: 100; 191 | opacity: 1; 192 | transition: all 0.1s ease-in-out; 193 | } 194 | 195 | .logged-in header .welcome-info .user-options ul { 196 | list-style-type: none; 197 | margin: 0; 198 | } 199 | 200 | .logged-in header .welcome-info .user-options ul button { 201 | padding: 5px 15px; 202 | border: none; 203 | background-color: transparent; 204 | color: var(--darkest-color); 205 | font-size: 1em; 206 | text-align: center; 207 | } 208 | 209 | .logged-in header .welcome-info .user-options ul button:hover { 210 | color: var(--secondary-color); 211 | } 212 | 213 | 214 | /* Main Content */ 215 | 216 | .logged-in main { 217 | display: flex; 218 | flex-wrap: wrap; 219 | justify-content: space-between; 220 | flex: 1; 221 | } 222 | 223 | 224 | /* Recent Chat Names on left */ 225 | 226 | .recent-chat { 227 | width: 30%; 228 | background-color: var(--sidebar-color); 229 | display: flex; 230 | flex-flow: column; 231 | } 232 | 233 | .recent-chat ul { 234 | overflow-y: scroll; 235 | flex: 1 1 1px; 236 | } 237 | 238 | .recent-chat ul li { 239 | border-bottom: 2px solid var(--sidebar-highlight-color); 240 | } 241 | 242 | .recent-chat ul li.selected { 243 | border: none; 244 | background-color: var(--sidebar-highlight-color); 245 | } 246 | 247 | .recent-chat ul li div { 248 | padding: 20px; 249 | } 250 | 251 | .recent-chat ul li h3.name { 252 | color: var(--main-color); 253 | font-size: 1.3em; 254 | transition: all 0.5s ease-in-out; 255 | } 256 | 257 | .recent-chat ul li:hover h3.name { 258 | color: var(--secondary-color); 259 | transition: all 0.5s ease-in-out; 260 | } 261 | 262 | .recent-chat ul li span.latest-message { 263 | color: var(--darkest-color); 264 | font-style: italic; 265 | padding: 5px 0 0; 266 | display: block; 267 | white-space: nowrap; 268 | overflow: hidden; 269 | text-overflow: ellipsis; 270 | } 271 | 272 | .recent-chat li.error-msg { 273 | color: var(--main-color); 274 | } 275 | 276 | .recent-chat .error-btn { 277 | background-color: var(--secondary-color); 278 | color: white; 279 | border: 0; 280 | padding: 10px; 281 | border-radius: 5px; 282 | margin-top: 10px; 283 | font-size: 0.9em; 284 | display: block; 285 | } 286 | 287 | .recent-chat .error-btn:hover { 288 | background-color: var(--secondary-lighter); 289 | } 290 | 291 | 292 | /* Current Chat Box on right */ 293 | 294 | .current-chat { 295 | width: 70%; 296 | display: flex; 297 | justify-content: space-between; 298 | flex-flow: column; 299 | height: 100%; 300 | } 301 | 302 | 303 | /* Chat Home */ 304 | 305 | .current-chat .chat-home { 306 | width: 100%; 307 | height: 100%; 308 | display: flex; 309 | align-items: center; 310 | justify-content: center; 311 | } 312 | 313 | .current-chat .chat-home button { 314 | background-color: var(--secondary-color); 315 | color: white; 316 | border: 0; 317 | padding: 15px; 318 | margin: 10px auto; 319 | font-weight: bold; 320 | font-size: 1.1em; 321 | border-radius: 10px; 322 | } 323 | 324 | .current-chat .chat-home button:hover { 325 | background-color: var(--secondary-lighter); 326 | } 327 | 328 | 329 | /* About the chatter at top */ 330 | 331 | .current-chat .user-info { 332 | margin: 15px 20px 0; 333 | padding: 0 0 10px 0; 334 | border-bottom: 2px solid var(--sidebar-highlight-color); 335 | text-align: center; 336 | } 337 | 338 | .current-chat .user-info h2.chatter-name { 339 | color: var(--main-color); 340 | font-size: 1.8em; 341 | font-weight: bold; 342 | padding: 3px; 343 | } 344 | 345 | .current-chat .user-info p { 346 | color: var(--mid-color); 347 | padding: 5px; 348 | } 349 | 350 | .current-chat .user-info p.in-chat { 351 | font-size: 1.2em; 352 | font-weight: bold; 353 | } 354 | 355 | .current-chat .user-info p.chatter-info { 356 | font-style: italic; 357 | } 358 | 359 | .current-chat .user-info p.chatter-bio { 360 | color: var(--darkest-color); 361 | } 362 | 363 | 364 | /* Send Message Input at bottom */ 365 | 366 | .current-chat .send-message { 367 | display: flex; 368 | justify-content: space-between; 369 | padding: 0 10px 10px; 370 | } 371 | 372 | .current-chat .send-message input { 373 | flex: 1; 374 | background-color: white; 375 | color: var(--darkest-color); 376 | padding: 15px; 377 | font-size: 1em; 378 | border: 1px solid var(--sidebar-highlight-color); 379 | } 380 | 381 | .current-chat .send-message button { 382 | background-color: var(--secondary-lighter); 383 | color: white; 384 | padding: 10px 25px; 385 | border: none; 386 | font-size: 1.1em; 387 | font-weight: bolder; 388 | } 389 | 390 | .current-chat .send-message input::placeholder, 391 | .unlogged-page .forms textarea::placeholder { 392 | color: var(--mid-color); 393 | font-style: italic; 394 | } 395 | 396 | 397 | /* Chat box in middle */ 398 | 399 | .current-chat .messages { 400 | flex: 1 1 1px; 401 | padding: 0 10px; 402 | display: flex; 403 | overflow: auto; 404 | display: flex; 405 | flex-direction: column-reverse; 406 | } 407 | 408 | .current-chat .messages ol { 409 | display: inline-block; 410 | list-style-type: none; 411 | width: 100%; 412 | } 413 | 414 | .current-chat .messages ol.short { 415 | align-self: flex-end; 416 | } 417 | 418 | .current-chat .messages ol li { 419 | display: block; 420 | margin: 10px; 421 | } 422 | 423 | .current-chat .messages ol li .message { 424 | padding: 12px 15px; 425 | border-radius: 10px; 426 | max-width: 50%; 427 | width: auto; 428 | display: inline-block; 429 | color: white; 430 | line-height: 1.3; 431 | } 432 | 433 | .current-chat .messages ol li.received { 434 | text-align: left; 435 | } 436 | 437 | .current-chat .messages ol li.sent { 438 | text-align: right; 439 | } 440 | 441 | .current-chat .messages ol li.received .message { 442 | background-color: var(--secondary-color); 443 | } 444 | 445 | .current-chat .messages ol li.sent .message { 446 | background-color: var(--main-color); 447 | } 448 | 449 | .current-chat .messages ol li p.sent-time { 450 | color: var(--mid-color); 451 | text-transform: uppercase; 452 | font-size: 0.9em; 453 | margin: 5px 5px 15px; 454 | } 455 | 456 | 457 | /* EXTRA */ 458 | 459 | 460 | /* Highlight selection */ 461 | 462 | ::-moz-selection { 463 | /* Code for Firefox */ 464 | color: var(--lightest-color); 465 | background: var(--secondary-lighter); 466 | } 467 | 468 | ::selection { 469 | color: var(--lightest-color); 470 | background: var(--secondary-lighter); 471 | } 472 | 473 | 474 | /* Scrollbar */ 475 | 476 | ::-webkit-scrollbar { 477 | width: 15px; 478 | } 479 | 480 | ::-webkit-scrollbar-track { 481 | background: #f6f9f9; 482 | } 483 | 484 | ::-webkit-scrollbar-thumb { 485 | background: #e0e9e9; 486 | border-radius: 7px; 487 | border: 2px solid #f6f9f9; 488 | } 489 | 490 | ::-webkit-scrollbar-thumb:hover { 491 | background: #afb5b6; 492 | } 493 | 494 | 495 | /* @MEDIA QUERIES FOR RESPONSIVE SITE */ 496 | 497 | 498 | /* Max width: 1024 */ 499 | 500 | @media screen and (max-width: 1024px) { 501 | .unlogged-page .logo { 502 | width: calc(40% - 40px); 503 | margin: 0 20px; 504 | } 505 | .unlogged-page .forms { 506 | width: calc(60% - 40px); 507 | } 508 | .unlogged-page .forms button { 509 | font-size: 1em; 510 | } 511 | } 512 | 513 | 514 | /* Max width: 768 */ 515 | 516 | @media screen and (max-width: 768px) { 517 | .unlogged-page { 518 | flex-direction: column; 519 | } 520 | .unlogged-page .logo { 521 | margin: 0; 522 | width: 80%; 523 | padding: 0 30px 30px; 524 | } 525 | .unlogged-page .forms { 526 | width: 80%; 527 | margin: 0; 528 | font-size: 1.1em; 529 | align-items: center; 530 | } 531 | .unlogged-page .forms button { 532 | margin: 0; 533 | align-items: center; 534 | font-size: 1em; 535 | border: none; 536 | } 537 | .current-chat .messages ol li .message { 538 | max-width: 60%; 539 | } 540 | } 541 | 542 | 543 | /* Max width: 680 */ 544 | 545 | @media screen and (max-width: 680px) { 546 | body { 547 | font-size: 18px; 548 | } 549 | .unlogged-page .logo, 550 | .unlogged-page .forms { 551 | width: 100%; 552 | } 553 | .open-toggle, 554 | .close-toggle { 555 | display: block; 556 | } 557 | .logged-in header .open-toggle { 558 | width: 100%; 559 | margin: 15px 0 25px; 560 | } 561 | .logged-in header .open-toggle a { 562 | color: white; 563 | background-color: var(--secondary-color); 564 | padding: 10px; 565 | border-radius: 5px; 566 | } 567 | .logged-in main { 568 | position: relative; 569 | } 570 | .recent-chat { 571 | width: 80%; 572 | position: absolute; 573 | top: 0; 574 | bottom: 0; 575 | left: -100%; 576 | transition: all 0.5s ease-in-out; 577 | } 578 | .recent-chat a.close-toggle { 579 | position: absolute; 580 | left: calc(100% + 15px); 581 | top: 10px; 582 | color: white; 583 | font-size: 2em; 584 | } 585 | .recent-chat a.close-toggle:hover { 586 | color: var(--sidebar-highlight-color); 587 | transform: rotate(180deg); 588 | } 589 | .recent-chat::before { 590 | content: ''; 591 | width: 25%; 592 | background-color: rgba(0, 0, 0, 0.3); 593 | transition: all 0.5s ease-in-out; 594 | position: absolute; 595 | top: 0; 596 | left: 0; 597 | bottom: 0; 598 | } 599 | .recent-chat:target { 600 | left: 0; 601 | transition: all 0.5s ease-in-out; 602 | } 603 | .recent-chat:target::before { 604 | transition: all 0.5s ease-in-out; 605 | left: 100%; 606 | } 607 | .current-chat { 608 | width: 100%; 609 | } 610 | .current-chat .messages ol li .message { 611 | max-width: 70%; 612 | } 613 | } 614 | 615 | 616 | /* Max width 480 */ 617 | 618 | @media screen and (max-width: 480px) { 619 | .recent-chat li.error-msg { 620 | margin: 0 50px 0 0; 621 | } 622 | .logged-in header .logo { 623 | width: 100%; 624 | padding: 10px 0; 625 | } 626 | .logged-in header .logo img { 627 | height: auto; 628 | width: 100%; 629 | margin: 0; 630 | } 631 | .logged-in header .open-toggle { 632 | width: auto; 633 | order: 2; 634 | font-size: 0.9em; 635 | } 636 | .logged-in header .welcome-info { 637 | order: 3; 638 | margin-bottom: 15px; 639 | } 640 | .recent-chat { 641 | width: 100%; 642 | } 643 | .recent-chat a.close-toggle { 644 | left: calc(100% - 38px); 645 | color: var(--secondary-lighter); 646 | } 647 | .recent-chat a.close-toggle:hover { 648 | color: var(--secondary-color); 649 | } 650 | .recent-chat::before { 651 | display: none; 652 | } 653 | .current-chat .messages ol li .message { 654 | max-width: 80%; 655 | } 656 | } -------------------------------------------------------------------------------- /public/assets/images/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JColeCodes/randm/7371875a62abc029bb34074a35b1282c7ceb8558/public/assets/images/logo-dark.png -------------------------------------------------------------------------------- /public/assets/images/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JColeCodes/randm/7371875a62abc029bb34074a35b1282c7ceb8558/public/assets/images/logo-light.png -------------------------------------------------------------------------------- /public/assets/images/randm-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JColeCodes/randm/7371875a62abc029bb34074a35b1282c7ceb8558/public/assets/images/randm-icon.png -------------------------------------------------------------------------------- /public/assets/js/delete-user.js: -------------------------------------------------------------------------------- 1 | async function deleteUser() { 2 | const logoutBtn = document.getElementById('logout-btn'); 3 | 4 | const currentUserId = parseInt( 5 | document.querySelector('#random-btn').getAttribute('data-user') 6 | ); 7 | 8 | const response = await fetch('/api/users/' + currentUserId, { 9 | method: 'delete', 10 | headers: { 'Content-Type': 'application/json' } 11 | }); 12 | 13 | if (response.ok) { 14 | logoutBtn.click(); // Logs user out when account is deleted 15 | } else { 16 | alert(response.statusText); 17 | } 18 | } 19 | 20 | document.querySelector('#delete-btn').addEventListener('click', deleteUser); -------------------------------------------------------------------------------- /public/assets/js/login.js: -------------------------------------------------------------------------------- 1 | async function loginFormHandler(event) { 2 | event.preventDefault(); 3 | 4 | const email = document.querySelector('#email-login').value.trim(); 5 | const password = document.querySelector('#password-login').value.trim(); 6 | 7 | if (email && password) { 8 | const response = await fetch('/api/users/login', { 9 | method: 'post', 10 | body: JSON.stringify({ 11 | email, 12 | password 13 | }), 14 | headers: { 'Content-Type': 'application/json' } 15 | }); 16 | 17 | if (response.ok) { 18 | document.location.replace('/'); 19 | } else { 20 | alert(response.statusText); 21 | } 22 | } 23 | } 24 | 25 | document.querySelector('.login-form').addEventListener('submit', loginFormHandler); -------------------------------------------------------------------------------- /public/assets/js/logout.js: -------------------------------------------------------------------------------- 1 | async function logout() { 2 | const response = await fetch('/api/users/logout', { 3 | method: 'post', 4 | headers: { 'Content-Type': 'application/json' } 5 | }); 6 | 7 | if (response.ok) { 8 | document.location.replace('/'); 9 | } else { 10 | alert(response.statusText); 11 | } 12 | } 13 | 14 | document.querySelector('#logout-btn').addEventListener('click', logout); -------------------------------------------------------------------------------- /public/assets/js/message.js: -------------------------------------------------------------------------------- 1 | async function sendMessageFormHandler(event) { 2 | event.preventDefault(); 3 | 4 | const message_text = document.querySelector('#message').value.trim(); 5 | // Get the id of the logged in user in the data-user attribute 6 | const sender_id = document.querySelector('#send-message-btn').getAttribute('data-user'); 7 | // Get the end of the URL and assign to receiver_id 8 | let receiver_id = window.location.toString().split('/'); 9 | receiver_id = parseInt(receiver_id[receiver_id.length - 1].split('#')[0]); 10 | 11 | if (message_text) { 12 | const response = await fetch('/api/messages', { 13 | method: 'POST', 14 | body: JSON.stringify({ 15 | sender_id, 16 | receiver_id, 17 | message_text 18 | }), 19 | headers: { 'Content-Type': 'application/json' } 20 | }); 21 | 22 | if (!response.ok) { 23 | alert(response.statusText); 24 | } 25 | } 26 | } 27 | 28 | document.querySelector('.send-message').addEventListener('submit', sendMessageFormHandler); -------------------------------------------------------------------------------- /public/assets/js/random-chat.js: -------------------------------------------------------------------------------- 1 | async function getOtherUserId() { 2 | // Get current user's id 3 | const currentUserId = parseInt( 4 | document.querySelector('#random-btn').getAttribute('data-user') 5 | ); 6 | 7 | // Get all users length as maximum 8 | fetch('/api/users') 9 | .then((res) => { 10 | return res.json(); 11 | }) 12 | .then((totalUsers) => { 13 | const usersArray = []; 14 | 15 | // Loop through each user and add them to a list if they are not current user 16 | totalUsers.forEach((user) => { 17 | if (user.id !== currentUserId) { 18 | usersArray.push(user.id); 19 | } 20 | }); 21 | 22 | checkCurrentMessages(usersArray); 23 | }); 24 | } 25 | 26 | // Get recent messages and compare users against all users 27 | async function checkCurrentMessages(usersArray) { 28 | fetch('/api/messages/recent', { method: 'GET' }) 29 | .then((response) => response.json()) 30 | .then((data) => { 31 | if (data.length > 0) { 32 | // Iterate through recent messages to make sure user isn't already chatting 33 | data.forEach((user) => { 34 | // If user exists, remove from user array 35 | if (usersArray.includes(user.id)) { 36 | usersArray.splice(usersArray.indexOf(user.id), 1); 37 | } 38 | }); 39 | } 40 | // Get random number based on number of users (0 to array length) 41 | const randomUserId = Math.floor(Math.random() * usersArray.length); 42 | // Go to url for that user 43 | document.location.replace(`/chat/${usersArray[randomUserId]}`); 44 | }); 45 | } 46 | 47 | document.getElementById('random-btn').addEventListener('click', getOtherUserId); -------------------------------------------------------------------------------- /public/assets/js/recent.js: -------------------------------------------------------------------------------- 1 | const recentList = document.querySelector('#recent-list'); 2 | 3 | async function displayRecentChat() { 4 | // Get recent messages 5 | const response = await fetch('/api/messages/recent', { method: 'GET' }); 6 | const data = await response.json(); 7 | 8 | // If data exists 9 | if (data.length > 0) { 10 | // Split page url 11 | var pageUrl = document.location.href.split('/'); 12 | pageUrl = pageUrl[pageUrl.length - 1].split('#')[0]; 13 | 14 | // For each recent chatters to display, create list element 15 | data.forEach((element) => { 16 | let recentLi = document.createElement('li'); 17 | recentLi.setAttribute('id', `user-${element.id}`); 18 | 19 | // If currently on the page of, show it as selected 20 | if (element.id == pageUrl) { 21 | recentLi.className = 'selected'; 22 | } 23 | 24 | // Link and text for display 25 | recentLi.innerHTML = `
26 |

${ 27 | element.first_name.charAt(0).toUpperCase() + 28 | element.first_name.slice(1) 29 | } ${element.last_name.charAt(0).toUpperCase()}.

30 | ${element.latest_message} 31 |
`; 32 | 33 | // Append to page 34 | recentList.appendChild(recentLi); 35 | }); 36 | } else { 37 | // If no data, display no chat message 38 | recentList.innerHTML = `
  • You currently have no chats.
  • `; 39 | } 40 | } 41 | 42 | // If recent chat api fails to load, display error message 43 | displayRecentChat().catch( 44 | (response) => 45 | (recentList.innerHTML = `
  • There's been an error displaying the recent chat list.
  • `) 46 | ); -------------------------------------------------------------------------------- /public/assets/js/register.js: -------------------------------------------------------------------------------- 1 | async function signupFormHandler(event) { 2 | event.preventDefault(); 3 | 4 | const email = document.querySelector('#email-register').value.trim(); 5 | const password = document.querySelector('#password-register').value.trim(); 6 | const first_name = document.querySelector('#firstname-register').value.trim(); 7 | const last_name = document.querySelector('#lastname-register').value.trim(); 8 | const gender = document.querySelector('#genders').value.trim(); 9 | const sexual_preference = document.querySelector('#sexual-preference').value.trim(); 10 | const pronouns = document.querySelector('#pronouns').value.trim(); 11 | const dob = document.querySelector('#birthday-register').value.trim(); 12 | const bio = document.querySelector('#bio').value.trim(); 13 | 14 | // Get birthday and calculate age 15 | const birthday = new Date(dob).toISOString().slice(0, 10); 16 | const dobDate = Date.parse(dob); 17 | 18 | function calculate_age(date) { 19 | const diff_ms = Date.now() - date; 20 | const age_dt = new Date(diff_ms); 21 | return Math.abs(age_dt.getUTCFullYear() - 1970); 22 | } 23 | 24 | const age = calculate_age(dobDate); 25 | 26 | if (age < 18) { 27 | // If user is not of age, they cannot register 28 | alert('Not of age!'); 29 | } else if (first_name && last_name && email && password && age > 18) { 30 | const response = await fetch('/api/users', { 31 | method: 'post', 32 | body: JSON.stringify({ 33 | first_name, 34 | last_name, 35 | email, 36 | password, 37 | bio, 38 | gender, 39 | sexual_preference, 40 | pronouns, 41 | birthday 42 | }), 43 | headers: { 'Content-Type': 'application/json' } 44 | }); 45 | 46 | if (response.ok) { 47 | logUserIn(email, password); 48 | } else { 49 | alert(response.statusText); 50 | } 51 | } 52 | } 53 | 54 | // Login function to automatically log user in when registering 55 | async function logUserIn(email, password) { 56 | const response = await fetch('/api/users/login', { 57 | method: 'post', 58 | body: JSON.stringify({ 59 | email, 60 | password 61 | }), 62 | headers: { 'Content-Type': 'application/json' } 63 | }); 64 | 65 | if (response.ok) { 66 | document.location.replace('/chat'); 67 | } else { 68 | alert(response.statusText); 69 | } 70 | } 71 | 72 | document.querySelector('.register-form').addEventListener('submit', signupFormHandler); -------------------------------------------------------------------------------- /public/assets/js/socket.js: -------------------------------------------------------------------------------- 1 | let currId = 0; 2 | let pageId = 0; 3 | 4 | const socket = io(); // SOCKET.IO 5 | 6 | // On submit button click, get information about the user who is sending a message 7 | document.querySelector('.send-message').addEventListener('submit', (event) => { 8 | event.preventDefault(); 9 | 10 | // Get id of message recipient 11 | pageId = document.location.href.split('/'); 12 | pageId = pageId[pageId.length - 1].split('#')[0]; 13 | if (pageId.includes('?')) { 14 | pageId = pageId.split('?')[0]; 15 | } 16 | 17 | // Get id of current user 18 | currId = document 19 | .querySelector('#send-message-btn') 20 | .getAttribute('data-user'); 21 | 22 | // If there is no message, do not continue 23 | if (document.querySelector('#message').value.trim() === '') { 24 | return; 25 | } 26 | // Emit the following information 27 | socket.emit('new message', { 28 | message: document.querySelector('#message').value, 29 | from: currId, 30 | to: pageId 31 | }); 32 | }); 33 | 34 | // Socket on taking in the information from the emit 35 | socket.on('new message', (data) => { 36 | // Message text content 37 | const message = data.message; 38 | // Ids of sender and recipient 39 | const fromId = data.from; 40 | const toId = data.to; 41 | // Get id of the user currently logged in (across all users on the page) 42 | const senderId = document 43 | .querySelector('#send-message-btn') 44 | .getAttribute('data-user'); 45 | // Get id of the recipient (across all users on the page) 46 | let pageUrl = document.location.href.split('/'); 47 | pageUrl = pageUrl[pageUrl.length - 1].split('#')[0]; 48 | if (pageUrl.includes('?')) { 49 | pageUrl = pageUrl.split('?')[0]; 50 | } 51 | 52 | // Live update of the recent message display 53 | const recentList = document.querySelector('#recent-list'); 54 | 55 | if (fromId == senderId) { 56 | let toRecentChatDiv = document.querySelector(`#user-${toId}`); 57 | // if div exists remove before adding to page 58 | if (toRecentChatDiv) { 59 | toRecentChatDiv.parentElement.removeChild(toRecentChatDiv); 60 | } 61 | // If the message is sent by current user 62 | let toNewRecentChatDiv = document.createElement('li'); 63 | toNewRecentChatDiv.setAttribute('id', `user-${toId}`); 64 | 65 | toNewRecentChatDiv.innerHTML = `

    66 | ${document.querySelector('.chatter-name').textContent} 67 |

    ${message}
    `; 68 | 69 | recentList.insertBefore(toNewRecentChatDiv, recentList.children[0]); 70 | 71 | // Put on top of the list 72 | } else if (toId == senderId) { 73 | let fromRecentChatDiv = document.querySelector(`#user-${fromId}`); 74 | // if div exists remove before adding to page 75 | if (fromRecentChatDiv) { 76 | fromRecentChatDiv.parentElement.removeChild(fromRecentChatDiv); 77 | } 78 | // If the message is sent by current user 79 | let fromNewRecentChatDiv = document.createElement('li'); 80 | fromNewRecentChatDiv.setAttribute('id', `user-${fromId}`); 81 | 82 | fromNewRecentChatDiv.innerHTML = `

    83 | ${document.querySelector('.chatter-name').textContent} 84 |

    ${message}
    `; 85 | 86 | recentList.insertBefore(fromNewRecentChatDiv, recentList.children[0]); 87 | } 88 | 89 | 90 | // Live update of messages 91 | if ( 92 | // If message is sent by current user AND the recipient is in the url 93 | (fromId == senderId && toId == pageUrl) || 94 | // OR message is sent by the user in the page url AND the recipient is current user 95 | (fromId == pageUrl && toId == senderId) 96 | ) { 97 | // Gets class name for message display depending on who sent/received 98 | const messageClass = (senderId, fromId) => { 99 | if (senderId != fromId) { 100 | return 'received'; 101 | } 102 | return 'sent'; 103 | }; 104 | // Get time at message send 105 | function msgTime() { 106 | return new Date().toLocaleString('en-US', { 107 | hour12: true, 108 | hourCycle: 'h12', 109 | hour: 'numeric', 110 | minute: '2-digit' 111 | }); 112 | } 113 | 114 | // Create list element with the message and send time 115 | let messageLi = document.createElement('li'); 116 | messageLi.className = messageClass(senderId, fromId); 117 | messageLi.innerHTML = `
    ${message}
    118 |

    ${msgTime()}

    `; 119 | 120 | // Append to chat messages list 121 | document.querySelector('#chat-messages').appendChild(messageLi); 122 | 123 | // Clear message input text box 124 | document.querySelector('#message').value = ''; 125 | } 126 | // Reset currId and pageId once message is sent and page displays message 127 | currId = 0; 128 | pageId = 0; 129 | }); -------------------------------------------------------------------------------- /seeds/index.js: -------------------------------------------------------------------------------- 1 | const seedUsers = require('./user-seeds'); 2 | const seedMessages = require('./message-seeds'); 3 | 4 | const sequelize = require('../config/connection'); 5 | 6 | const seedAll = async() => { 7 | await sequelize.sync({ force: true }); 8 | console.log('\n----- DATABASE SYNCED -----\n'); 9 | 10 | await seedUsers(); 11 | console.log('\n----- USERS SEEDED -----\n'); 12 | 13 | await seedMessages(); 14 | console.log('\n----- MESSAGES SEEDED -----\n'); 15 | 16 | process.exit(0); 17 | }; 18 | 19 | seedAll(); -------------------------------------------------------------------------------- /seeds/message-seeds.js: -------------------------------------------------------------------------------- 1 | const { Message } = require('../models'); 2 | 3 | const messageData = [{ 4 | sender_id: 1, 5 | receiver_id: 2, 6 | message_text: 'Hi, how are you?' 7 | }, 8 | { 9 | sender_id: 2, 10 | receiver_id: 1, 11 | message_text: 'Hi there - good, how about you? What kind of music do you like?' 12 | }, 13 | { 14 | sender_id: 1, 15 | receiver_id: 2, 16 | message_text: 'I love basically everything, been listening to jazz a lot today. What about you?' 17 | }, 18 | { 19 | sender_id: 2, 20 | receiver_id: 1, 21 | message_text: 'Nice, I like jazz and hip hop a lot!' 22 | }, 23 | { 24 | sender_id: 2, 25 | receiver_id: 3, 26 | message_text: 'Hi, what is your favorite food?' 27 | }, 28 | { 29 | sender_id: 3, 30 | receiver_id: 2, 31 | message_text: 'Hi! I love Thai food. What about you?' 32 | }, 33 | { 34 | sender_id: 2, 35 | receiver_id: 3, 36 | message_text: 'Ooh I love Thai but my favorite is Italian' 37 | } 38 | ]; 39 | 40 | const seedMessages = () => Message.bulkCreate(messageData); 41 | 42 | module.exports = seedMessages; -------------------------------------------------------------------------------- /seeds/user-seeds.js: -------------------------------------------------------------------------------- 1 | const { User } = require('../models'); 2 | 3 | const userData = [ 4 | { 5 | first_name: 'Sam', 6 | last_name: 'Smith', 7 | email: 'samsmith@email.com', 8 | password: 'password1', 9 | bio: 'I listen to all kinds of music every day', 10 | gender: 'non-binary', 11 | sexual_preference: 'pansexual', 12 | pronouns: 'they/them', 13 | birthday: 03 / 03 / 2003, 14 | }, 15 | { 16 | first_name: 'Laura', 17 | last_name: 'Lee', 18 | email: 'lauralee@email.com', 19 | password: 'password1', 20 | bio: 'I have over 50 plants', 21 | gender: 'female', 22 | sexual_preference: 'bisexual', 23 | pronouns: 'she/her', 24 | birthday: 01 / 01 / 2000, 25 | }, 26 | { 27 | first_name: 'Frankie', 28 | last_name: 'Gray', 29 | email: 'frankie@email.com', 30 | password: 'password1', 31 | bio: 'I love hiking and travelling', 32 | gender: 'male', 33 | sexual_preference: 'homosexual', 34 | pronouns: 'he/they', 35 | birthday: 07 / 07 / 1997, 36 | }, 37 | ]; 38 | 39 | const seedUsers = () => User.bulkCreate(userData, { individualHooks: true }); 40 | 41 | module.exports = seedUsers; 42 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const routes = require('./controllers'); 4 | const sequelize = require('./config/connection'); 5 | const session = require('express-session'); 6 | const exphbs = require('express-handlebars'); 7 | const helpers = require('./utils/helpers'); 8 | const hbs = exphbs.create({ helpers }); 9 | require('dotenv').config(); 10 | 11 | const app = express(); 12 | const PORT = process.env.PORT || 3001; 13 | 14 | const server = require('http').createServer(app); 15 | const io = require('socket.io')(server); 16 | 17 | const SequelizeStore = require('connect-session-sequelize')(session.Store); 18 | // Setup for cookies use 19 | const sess = { 20 | secret: process.env.SECRET_SECRET, 21 | cookie: {}, 22 | resave: false, 23 | saveUninitialized: true, 24 | store: new SequelizeStore({ 25 | db: sequelize 26 | }) 27 | }; 28 | 29 | app.use(session(sess)); 30 | app.use(express.json()); 31 | app.use(express.urlencoded({ extended: true })); 32 | app.use(express.static(path.join(__dirname, 'public'))); 33 | 34 | // Turn on routes 35 | app.use(routes); 36 | 37 | // Handlebars 38 | app.engine('handlebars', hbs.engine); 39 | app.set('view engine', 'handlebars'); 40 | 41 | // Socket.io 42 | io.on('connection', (socket) => { 43 | socket.on('new message', (message) => { 44 | io.emit('new message', message); 45 | }); 46 | }); 47 | 48 | // Turn on connection to db and server 49 | sequelize.sync({ force: false }).then(() => { 50 | server.listen(PORT, () => console.log(`Now listening on port ${PORT}`)); 51 | }); -------------------------------------------------------------------------------- /utils/filters.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getUserLatest: (messages, user, sessionId, paramId) => { 3 | // Filters for the current user via session id 4 | var currentUser = user.filter((user) => user.id == sessionId); 5 | currentUser = currentUser[0]; 6 | 7 | // Filters for current chatter via param id 8 | var currentChatter = user.filter((user) => user.id == paramId); 9 | currentChatter = currentChatter[0]; 10 | if (!paramId) { 11 | currentChatter = null; // Returns null if on home /chat page 12 | } 13 | 14 | // Filters for an array of most recent chat messages per chatter 15 | let latestChat = []; 16 | if (messages) { 17 | messages.forEach((message) => { 18 | user.forEach((user) => { 19 | if ( 20 | // If sender is user and user id is sender id 21 | (message.sender_id !== sessionId && user.id === message.sender_id) || 22 | // OR if receiver is user and user id is sender id 23 | (message.receiver_id !== sessionId && user.id === message.receiver_id) 24 | ) { 25 | // Only do if latestChat array does not already include user 26 | if (!latestChat.includes(user)) { 27 | // Set latest message and push to array 28 | user.latest_message = message.message_text; 29 | latestChat.push(user); 30 | } 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | return { currentUser, currentChatter, latestChat }; 37 | } 38 | }; -------------------------------------------------------------------------------- /utils/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get_chat_class: (senderId, currentId) => { 3 | if (senderId !== currentId) { 4 | return 'received'; 5 | } 6 | return 'sent'; 7 | }, 8 | format_time: (time) => { 9 | return new Date(time).toLocaleString('en-US', { 10 | hour12: true, 11 | hourCycle: 'h12', 12 | hour: 'numeric', 13 | minute: '2-digit' 14 | }); 15 | }, 16 | capitalize_first_name: (string) => { 17 | return string.charAt(0).toUpperCase() + string.slice(1); 18 | }, 19 | last_name_initial: (string) => { 20 | return string.charAt(0).toUpperCase() + '.'; 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /views/chat.handlebars: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    4 | 5 | 8 | 9 |
    10 | {{#with userLatest.currentUser}} 11 | Hello, {{capitalize_first_name first_name}}! 12 | 13 | {{/with}} 14 | 15 |
    16 |
      17 |
    • 18 |
    • 19 |
    20 |
    21 |
    22 | 23 | 29 |
    30 | 31 | 32 |
    33 | 34 |
    35 |
      36 | 37 |
      38 | 39 | 40 | 41 |
      42 | {{#if chatHome}} 43 | 44 |
      45 | {{#with userLatest.currentUser}} 46 | 47 | {{/with}} 48 | 49 |
      50 | 51 | {{else}} 52 | 53 | 54 | 65 | 66 | 67 |
      68 |
        69 | {{#each messages}} 70 |
      1. 71 |
        {{message_text}}
        72 |

        {{format_time createdAt}}

        73 |
      2. 74 | {{/each}} 75 |
      76 |
      77 | 78 | 79 |
      80 | {{#with userLatest.currentChatter as |user|}} 81 | 83 | {{/with}} 84 | 85 | {{#with userLatest.currentUser}} 86 | 87 | {{/with}} 88 |
      89 | 90 | 91 | {{/if}} 92 |
      93 |
      94 |
      95 | 96 | 97 | -------------------------------------------------------------------------------- /views/layouts/main.handlebars: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RANDM 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {{{body}}} 20 | 21 | -------------------------------------------------------------------------------- /views/login.handlebars: -------------------------------------------------------------------------------- 1 |
      2 | 5 |
      6 | 19 |
      20 | Don't have an account? Register now! 21 |
      22 |
      23 |
      24 | 25 | 26 | -------------------------------------------------------------------------------- /views/register.handlebars: -------------------------------------------------------------------------------- 1 |
      2 | 5 |
      6 |
      7 |
      8 | 9 | 10 |
      11 |
      12 | 13 | 14 |
      15 |
      16 | 17 | 18 |
      19 |
      20 | 21 | 22 |
      23 |
      24 | 25 | 31 |
      32 |
      33 | 34 | 35 |
      36 |
      37 | 38 | 39 |
      40 |
      41 | 42 | 43 |
      44 |
      45 | 46 | 47 |
      48 |
      49 | 50 |
      51 |
      52 |
      53 | Already have an account? 54 | Login here! 55 |
      56 |
      57 |
      58 | 59 | --------------------------------------------------------------------------------