<repo_name>VeselAbaya/VectorWayContest-Chat-App<file_sep>/server/db/models/roomMessages.js
const mongoose = require('../mongoose');

const RoomMessagesSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true,
    minlength: 1,
    unique: 1,
  },
  messages: [
    {
      from: {
        type: String,
        trim: true,
      },
      text: {
        type: String,
        trim: true,
      },
      url: {
        type: String,
        trim: true,
      }
    }
  ]
});

const RoomMessages = mongoose.model('RoomMessages', RoomMessagesSchema);

module.exports = {RoomMessages};
<file_sep>/server/routes.js
const express = require('express');
const path = require('path');
const bodyParser = require('body-parser');

const mongoose = require('./db/mongoose');
const User = require('./db/models/user');
const {RoomMessages} = require('./db/models/roomMessages');
const authenticate = require('./middleware/authenticate');

const app = express();

const port = process.env.PORT || 3030;
const staticPath = path.join(__dirname, '../dist');

const roomsNames = require('./utils/roomsNames/roomsNames');
const availableRooms = new Set();

app.use(express.static(staticPath));
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json({extended: true}));

app.get('/rooms', (req, res) => {
  res.send(JSON.stringify(Array.from(availableRooms)))
});

app.get('/roomsnames/:roomName', (req, res) => {
  res.send(JSON.stringify(roomsNames.getNames(req.params.roomName)))
});

app.get('/roomsMessages/:roomName', (req, res) => {
  RoomMessages.findOne({name: req.params.roomName}, (err, doc) => {
    if (err)
      res.status(400).send(err);

    res.send(doc)
  })
});

app.get('/names', (req, res) => {
  User.find({}, (err, users) => {
    res.send(users.map(user => user.name))
  })
});

app.post('/signup', (req, res) => {
  const user = new User({
    name: req.body.name,
    email: req.body.email,
    password: req.body.password
  });

  user.save()
    .then(() => user.generateAuthToken())
    .then(token => {
      res.header('x-auth', token).send({user})
    })
    .catch(err => {
      res.status(400).json(err)
    })
});

app.post('/login', (req, res) => {
  User.findByCredentials(req.body.email, req.body.password)
    .then(user => {
      if (!user)
        res.status(400).send('Email or password is incorrect');

      user.generateAuthToken()
        .then(token => {
          res.header('x-auth', token).send({user})
        })
    })
    .catch(err => {
      res.status(400).send({err})
    })
});

app.get('/users/me', authenticate, (req, res) => {
  res.send(req.user)
});

app.post('/logout', authenticate, (req, res) => {
  res.sendStatus(200)
});

// for presentation
app.get('/vk', (req, res) => {
  res.redirect('https://vk.com/veselabaya')
});

app.get('/git', (req, res) => {
  res.redirect('https://github.com/VeselAbaya/VectorWayContest-Chat-App')
});

module.exports = {app, availableRooms, port, roomsNames};
<file_sep>/server/utils/users/users.test.js
const {Users} = require('./users');
const expect = require('expect');

describe('Users', () => {
  let users;

  beforeEach(() => {
    users = new Users();
    users.users = [{
        id: '1',
        name: 'Antay',
        room: 'Awesome room'
      },
      {
        id: '2',
        name: 'Mike',
        room: 'Awesome room'
      },
      {
        id: '3',
        name: 'Paul',
        room: 'Sosi room'
      }
    ]
  });

  it('Should add new user', () => {
    const user = {
      id: '/#124pogjeorjgdf',
      name: 'Antay1',
      room: 'Awesome room'
    };

    expect(users.addUser(user.id, user.name, user.room)).toEqual(user);
    expect(users.users.length).toBe(4)
  });

  it('Should return names for Awesome room', () => {
    expect(users.getUsersList('Awesome room')).toEqual(['Antay', 'Mike'])
  });

  it('Should remove user', () => {
    const removedUser = users.removeUser('3');

    expect(removedUser).toEqual({
      id: '3',
      name: 'Paul',
      room: 'Sosi room'
    });
    expect(users.getUsersList('Sosi room').length).toBe(0)
  });

  it('Shouldn\'t remove user', () => {
    expect(users.removeUser('1234')).toBeFalsy();
    expect(users.users.length).toBe(3)
  });

  it('Should find user', () => {
    expect(users.getUser('2')).toEqual(users.users[1])
  });

  it('Shouldn\'t find user', () => {
    expect(users.getUser('5')).toBeFalsy()
  })
});
<file_sep>/client/js/join/index.js
import "babel-polyfill";

import axios from 'axios'
import {elements} from "./elements"
import {popup, logOutClicked} from "./buttons"
import {signUpHandler, joinHandler, logInHandler} from "./submitHandlers"

const displayRooms = async () => {
  const rooms = await axios.get('/rooms');
  const datalist = elements.datalist;
  rooms.data.forEach(room => {
    const markup = `
      <option>${room}</option>
    `;

    datalist.insertAdjacentHTML('beforeend', markup)
  })
};

document.addEventListener('DOMContentLoaded', () => {
  displayRooms();

  // Buttons
  elements.popupLogInBtn.addEventListener('click', popup.logIn);
  elements.popupSignUpBtn.addEventListener('click', popup.signUp);
  elements.popupJoinBtn.addEventListener('click', popup.join);
  elements.logOutBtn.addEventListener('click', logOutClicked);

  // Sign Up Form
  elements.signUpForm.addEventListener('submit', signUpHandler);
  elements.signUpEmail.addEventListener('input', (event) => {
    event.target.classList.remove('form--input-error');
    event.target.parentNode.lastElementChild.style.display = 'none'
  });
  elements.signUpName.addEventListener('input', (event) => {
    event.target.classList.remove('form--input-error');
    event.target.parentNode.lastElementChild.style.display = 'none'
  });

  // Join Form
  elements.joinForm.addEventListener('submit', joinHandler);
  elements.joinName.addEventListener('input', (event) => {
    event.target.classList.remove('form--input-error');
    event.target.parentNode.lastElementChild.style.display = 'none'
  });

  // Login Form
  elements.logInForm.addEventListener('submit', logInHandler);

  const token = localStorage.getItem('token');
  if (token) {
    axios({
      url: '/users/me',
      method: 'get',
      headers: {'x-auth': localStorage.getItem('token')}
    }).then(res => {
      elements.joinName.value = res.data.name;

      // change "Log in" and "Sign up" button to "Log out" button
      elements.popupSignUpBtn.style.display = 'none';
      elements.popupLogInBtn.style.display = 'none';

      elements.logOutBtn.style.display = 'block'
    });
  }
});
<file_sep>/README.md
# VectorWayContest-Chat-App

Это приложение для первого VectorWay контеста.

## Getting Started

Склонируйте репозиторий и установите все зависимости

```
git clone https://github.com/VeselAbaya/VectorWayContest-Chat-App.git
npm install
```

### package.json scripts

```
{
  ...
  "scripts": {
    "dev": "webpack --watch",
    "start": "node server/server.js",
    "test": "mocha server/**/*.test.js",
    "test-watch": "nodemon --exec 'npm test'",
    "server": "nodemon server/server.js"
  },
  ...
}
```
* npm start - для heroku
* npm run server - типа hack-the-fucking-bank :)
* npm run test(-watch) - просто несколько тестов для парочки классов, используемых на сервере
* npm run dev - для начала разработки фронта транспиляция всех js и все такое... <br>

### server/config/config.json
В общем, конфиг файл выглядит так
```
{
  "PORT": 3030,
  "MONGODB_URI": "mongodb://localhost:27017/chatApp", 
  "JWT_SECRET": "KaaUsCHAasdRANDOMNIHqwertySIMVOLOV"
}
```
* PORT: Впишите здесь свой порт
* MONGODB_URI: Локальная база данных (27017 - вроде как стандартный порт)
* JWT_SECRET: Ваш JWT ключ <br>
Как он используется вы можете посмотреть в файле server/config/config.js

## Deployment on heroku

Изучите, по желанию, как деплоить на [heroku](https://devcenter.heroku.com/articles/git) <br>
Вообще, если заинтересованы, можете написать мне, и я буду деплоить сам (будет что-то типа командной разработки, будем работать над одним проектом - круто!)

## MONGODB

Также можете изучить, как работать с этой базой данных, и опять-таки, если будут любые вопросы, пишите мне в личку, и мы вместе попроубем разобраться!

## Authors

* **<NAME>** - [VK profile](https://vk.com/veselabaya) <br>
Пишите абсолютно со всеми вопросами и УПРЕКАМИ, буду рад выслушать, помочь и/или исправиться. 
<file_sep>/client/js/chat/animation.js
import {elements} from "./elements"

export const animateMenu = () => {
  elements.container.style.transition = '.5s';
  elements.menuBtn.style.transition = '.5s';
  elements.usersIcon.style.transition = '.25s';
  if (elements.container.style.transform === 'translateX(-13em)' || !elements.container.style.transform) {
    elements.container.style.transform = 'translateX(0)';
    elements.menuBtn.style.transform = 'translateX(14.9em)';
    elements.usersIcon.style.color = '#fff'
  }
  else {
    elements.container.style.transform = 'translateX(-13em)';
    elements.menuBtn.style.transform = 'translateX(17.5em)';
    elements.usersIcon.style.color = '#000'
  }
};
<file_sep>/client/js/join/buttons.js
import axios from 'axios'
import {elements} from "./elements"

const signUp = () => {
  elements.formsWrapper.style.top = `${-2*462.5}px`;
  elements.forms.style.height = '600px';

  elements.signUpForm.style.visibility = 'visible';
  setTimeout(() => {
    elements.joinForm.style.visibility = 'hidden';
    elements.logInForm.style.visibility = 'hidden';
  }, 300)
};

const logIn = () => {
  elements.forms.style.height = '462.5px';
  elements.formsWrapper.style.top = `-462.5px`;

  elements.logInForm.style.visibility = 'visible';
  setTimeout(() => {
    elements.joinForm.style.visibility = 'hidden';
    elements.signUpForm.style.visibility = 'hidden';
  }, 300)
};

const join = () => {
  elements.forms.style.height = '462.5px';
  elements.formsWrapper.style.top = 0;

  elements.joinForm.style.visibility = 'visible';
  setTimeout(() => {
    elements.logInForm.style.visibility = 'hidden';
    elements.signUpForm.style.visibility = 'hidden';
  }, 300)
};

export const popup = {
  signUp, logIn, join
};

export const logOutClicked = () => {
  // logout
  // post request with token - then on server find user by token and delete it from
  // tokens array, so that user can never get access to private routes
  axios({
    url: '/logout',
    method: 'post',
    headers: {'x-auth': localStorage.getItem('token')}
  })
    .then(res => {
      localStorage.setItem('token', '');

      // change UI
      elements.joinName.value = '';
      elements.logOutBtn.style.display = 'none';
      elements.popupLogInBtn.style.display = 'block';
      elements.popupSignUpBtn.style.display = 'block'
    })
    .catch(err => {
      alert(err)
    })
};
<file_sep>/server/utils/users/users.js
class Users {
  constructor() {
    this.users = []
  }

  addUser(id, name, room) {
    const user = {id, name, room};
    if (!this.users.find(user => user.name === name))
      this.users.push(user);
    return user
  }

  removeUser(id) {
    const delIndex= this.users.findIndex(el => el.id === id);
    const delUser = this.users[delIndex];
    if (delIndex !== -1)
      this.users.splice(delIndex, 1);

    return delUser
  }

  getUsersList(room) {
    return this.users.filter((el) => el.room === room)
      .map(user => user.name)
  }

  getUser(id) {
    const index = this.users.findIndex(el => el.id === id);
    return this.users[index]
  }
}

module.exports = {Users};

<file_sep>/server/utils/roomsNames/roomsNames.test.js
const expect = require('expect');
const roomsNames = require('./roomsNames');

describe('RoomsNames', () => {
  it('should add new name into LOTR room', () => {
    roomsNames.addName('LOTR', 'Antay');
    expect(roomsNames.data.find(roomNames => roomNames.name === 'lotr').names[0])
      .toBe('antay')
  });

  it('should get all names from LOTR room', () => {
    roomsNames.addName('loTr', 'Katya');
    expect(roomsNames.getNames('lotr')).toEqual(['antay', 'katya'])
  });

  it('should remove name from LOTR room', () => {
    roomsNames.removeName('loTR', 'Antay');
    expect(roomsNames.getNames('lotr')).toEqual(['katya'])
  })
});
<file_sep>/server/server.js
require('./config/config');

const http = require('http');
const socketIO = require('socket.io');
const {app, availableRooms, port, roomsNames} = require('./routes');

const server = http.createServer(app);
const io = socketIO(server);

const {RoomMessages} = require('./db/models/roomMessages');

const {generateMessage, generateLocationMessage} = require('./utils/messages/message');
const {isRealString} = require('./utils/validation');

const {Users} = require('./utils/users/users');
const users = new Users();

const saveMessage = (roomName, message) => {
  let newMessages;
  RoomMessages.findOne({name: roomName})
    .then(doc => {
      // console.log('doc', doc)
      if (doc.messages)
        newMessages = [...doc.messages, message];
      else
        newMessages = [message];

      // console.log(newMessages)
      RoomMessages.findOneAndUpdate(
        {name: roomName},
        {$set: {messages: newMessages}})
        .then(room => {
          console.log(room)
        })
        .catch(err => {
          console.log(err)
        })
    })
    .catch(err => console.log('error'))
};

io.on('connection', (socket) => {
  socket.on('join', (params, callback) => {
    if (!isRealString(params.name) || !isRealString(params.room))
      return callback('Name and room name are required');

    // create new document in DB for that room
    const roomMessages = new RoomMessages({
      name: params.room,
      messages: []
    });
    roomMessages.save()
      .catch(err => {
        console.log(err)
      });

    // Connect user with the room
    socket.join(params.room);
    users.removeUser(socket.id);
    users.addUser(socket.id, params.name, params.room);
    io.to(params.room).emit('updateUserList', users.getUsersList(params.room));

    // update available rooms
    if (users.getUsersList(params.room).length >= 1)
      availableRooms.add(params.room);

    // update roomsNames
    roomsNames.addName(params.room, params.name);

    socket.on('createMessage', message => {
      const user = users.getUser(socket.id);

      if (user && isRealString(message.text)) {
        io.to(params.room)
          .emit('newMessage', generateMessage(user.name, message.text));

        saveMessage(params.room, message)
      }
    });

    socket.on('createLocationMessage', (coords) => {
      const user = users.getUser(socket.id);

      const message = generateLocationMessage(user.name, coords.lat, coords.lon);
      if (user) {
        io.to(params.room)
          .emit('newLocationMessage', message);

        saveMessage(params.room, message)
      }
    })
  });

  socket.on('disconnect', () => {
    const user = users.removeUser(socket.id);

    // update roomsNames
    if (user)
      roomsNames.removeName(user.room, user.name);

    if (user)
      io.to(user.room).emit('updateUserList', users.getUsersList(user.room).sort());

    if (user && users.getUsersList(user.room).length <= 0)
      availableRooms.delete(user.room)
  })
});

server.listen(port, () => {
  console.log(`Server started up on ${port}`)
});
<file_sep>/client/js/chat/chat.js
import {elements} from "./elements"
import {animateMenu} from "./animation"
import {onNewMessage, onNewLocationMessage, updateUserList, fetchMessages} from "./webSocketHandlers"

const queryString = require('query-string');

console.log('client started up');
const socket = io(); // emit 'connection'

document.addEventListener('DOMContentLoaded', () => {
  fetchMessages();
  const params = queryString.parse(window.location.search);

  elements.sendLocationBtn.addEventListener('click', (event) => {
    event.preventDefault();
    if (!navigator.geolocation)
      return alert('Geolocation not supported by your browser :(');

    navigator.geolocation.getCurrentPosition((position) => {
      socket.emit('createLocationMessage', {
        lat: position.coords.latitude,
        lon: position.coords.longitude
      })
    }, () => {
      alert('Unable to fetch location :(')
    })
  });

  elements.sendForm.addEventListener('submit', (e) => {
    e.preventDefault();

    const input = elements.messageInput;
    if (input.value)
      socket.emit('createMessage', {
        from: params.name,
        text: input.value
      });

    input.value = ''
  });

  socket.on('connect', () => {
    socket.emit('join', {name: params.name, room: params.room.toLowerCase()}, (err) => {
      if (err) {
        alert(err);
        window.location.href = '/'
      }
    })
  });
  socket.on('newMessage', onNewMessage);
  socket.on('newLocationMessage', onNewLocationMessage);
  socket.on('updateUserList', updateUserList);

  elements.menuBtn.addEventListener('click', animateMenu)
});
<file_sep>/server/utils/messages/message.test.js
const expect = require('expect');
const {generateMessage, generateLocationMessage} = require('./message');

describe('generateMessage', () => {
  it('should generate correct message object', () => {
    const message = generateMessage('Admin', 'Soset bibu');
    expect(message.from).toBe('Admin');
    expect(message.text).toBe('Soset bibu');
    expect(typeof message.createdAt).toBe('number')
  })
});

describe('generateLocationMessage', () => {
  it('should generate correct location object', () => {
    const from = "Admin";
    const lat = 15;
    const lon = 19;
    const url = `https://www.google.com/maps?q=${lat},${lon}`;
    const message = generateLocationMessage(from, lat, lon);

    expect(typeof message.createdAt).toBe('number');
    expect(message.url).toBe(url)
  })
});
