├── .gitignore ├── LICENSE ├── README.md ├── config ├── default.json └── passport.js ├── models └── User.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── routes └── api │ └── users.js ├── server.js ├── src ├── App.css ├── App.js ├── App.test.js ├── actions │ ├── authActions.js │ └── types.js ├── components │ ├── auth │ │ ├── Login.js │ │ └── Register.js │ ├── dashboard │ │ ├── Dashboard.js │ │ └── dashboard.css │ ├── editor │ │ ├── editor.css │ │ └── editor.js │ ├── index.js │ ├── layout │ │ ├── Landing.js │ │ └── Navbar.js │ ├── mainpage │ │ ├── codeShare.gif │ │ ├── mainpage.css │ │ └── mainpage.js │ └── private-route │ │ └── PrivateRoute.js ├── index.js ├── reducers │ ├── authReducers.js │ ├── errorReducers.js │ └── index.js ├── serviceWorker.js ├── setupTests.js ├── store.js └── utils │ └── setAuthToken.js └── validation ├── login.js └── register.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | /config 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Divyam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### https://livecodeshare.herokuapp.com/ 2 | 3 | # CodeLive 4 | 5 | An easy to use Collaborative Code Editor written using Node.js, React, Redux, Socket.io and MongoDB. 6 | 7 | Supports auto completion and syntax highlighting for multiple languages. No limit on participants. It uses the [React wrapper](https://github.com/suren-atoyan/monaco-react#readme) for Monaco editor. 8 | 9 | ![image](https://github.com/Nobitaaah/code-live/blob/master/src/components/mainpage/codeShare.gif) 10 | 11 | ## Getting started 12 | 13 | ```bash 14 | # Download 15 | git clone https://github.com/Nobitaaah/code-live 16 | 17 | cd code-live 18 | 19 | npm install 20 | 21 | # You need to setup a MongoDB Atlas then replace the config vars in ./config. 22 | 23 | node server.js 24 | 25 | npm run start 26 | 27 | # Go to http://localhost:3000 to see it live. 28 | ``` 29 | The code editor won't work on mobile as Monaco Editor has weird issues with touch devices. Will add a read-only mode for mobile users in the future. 30 | 31 | ### TODO 32 | 33 | - [ ] Add tests 34 | - [ ] Add an option to save code 35 | - [ ] Add an option for real-time compilation 36 | - [ ] Allow webcam streaming 37 | - [ ] Add admin controls for creator of room 38 | - [ ] Use Docker 39 | - [ ] Add support for mobile 40 | - [ ] Improve code quality 41 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "CodeLive": { 3 | "dbConfig": { 4 | "uri": "mongodb+srv: your uri" 5 | }, 6 | "secret": "secretkeyhere" 7 | } 8 | } -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | const JwtStratergy = require("passport-jwt").Strategy; 2 | const ExtractJwt = require("passport-jwt").ExtractJwt; 3 | const mongoose = require("mongoose"); 4 | const User = mongoose.model("users"); 5 | const config = require('config'); 6 | 7 | const secret = config.get('CodeLive.secret'); 8 | 9 | const opts = {}; 10 | opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken(); 11 | opts.secretOrKey = secret; 12 | 13 | module.exports = (passport) => { 14 | passport.use( 15 | new JwtStratergy(opts, (jwt_payload, done) => { 16 | User.findById(jwt_payload.id) 17 | .then((user) => { 18 | if (user) { 19 | return done(null, user); 20 | } 21 | return done(null, false); 22 | }) 23 | .catch((err) => console.log(err)); 24 | }) 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const UserSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | email: { 10 | type: String, 11 | required: true, 12 | }, 13 | password: { 14 | type: String, 15 | required: true, 16 | }, 17 | date: { 18 | type: Date, 19 | default: Date.now, 20 | }, 21 | }); 22 | 23 | module.exports = User = mongoose.model("users", UserSchema); 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-live", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@monaco-editor/react": "^3.6.3", 7 | "@testing-library/jest-dom": "^4.2.4", 8 | "@testing-library/react": "^9.3.2", 9 | "@testing-library/user-event": "^7.1.2", 10 | "axios": "^0.20.0", 11 | "bcryptjs": "^2.4.3", 12 | "body-parser": "^1.19.0", 13 | "classnames": "^2.2.6", 14 | "concurrently": "^5.3.0", 15 | "config": "^3.3.2", 16 | "express": "^4.17.1", 17 | "gsap": "^3.5.1", 18 | "is-empty": "^1.2.0", 19 | "jsonwebtoken": "^8.5.1", 20 | "jwt-decode": "^3.0.0", 21 | "monaco-editor": "^0.21.2", 22 | "mongoose": "^5.10.7", 23 | "passport": "^0.4.1", 24 | "passport-jwt": "^4.0.0", 25 | "react": "^16.13.1", 26 | "react-device-detect": "^1.14.0", 27 | "react-dom": "^16.13.1", 28 | "react-icons": "^3.11.0", 29 | "react-redux": "^7.2.1", 30 | "react-router-dom": "^5.2.0", 31 | "react-scripts": "3.4.3", 32 | "redux": "^4.0.5", 33 | "redux-thunk": "^2.3.0", 34 | "socket.io": "^2.3.0", 35 | "socket.io-client": "^2.3.1", 36 | "validator": "^13.1.17" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "proxy": "http://localhost:5000/", 45 | "eslintConfig": { 46 | "extends": "react-app" 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nobitaaah/code-live/accb3a91693d01d8bf0bdba99c666e65a5af7f56/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 19 | 23 | 24 | 33 | React App 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nobitaaah/code-live/accb3a91693d01d8bf0bdba99c666e65a5af7f56/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nobitaaah/code-live/accb3a91693d01d8bf0bdba99c666e65a5af7f56/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /routes/api/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const bcrypt = require("bcryptjs"); 4 | const jwt = require("jsonwebtoken"); 5 | const config = require('config'); 6 | 7 | const validateRegisterInput = require("../../validation/register"); 8 | const validateLoginInput = require("../../validation/login"); 9 | 10 | const User = require("../../models/User"); 11 | const secret = config.get('CodeLive.secret'); 12 | 13 | 14 | // @route POST api/users/register 15 | // @desc Register User 16 | // @access Public 17 | router.post("/register", (req, res) => { 18 | const { errors, isValid } = validateRegisterInput(req.body); 19 | 20 | if (!isValid) { 21 | return res.status(400).json(errors); 22 | } 23 | 24 | User.findOne({ email: req.body.email }).then((user) => { 25 | if (user) { 26 | return res.status(400).json({ email: "Email already exists" }); 27 | } else { 28 | const newUser = new User({ 29 | name: req.body.name, 30 | email: req.body.email, 31 | password: req.body.password, 32 | }); 33 | // Hash password before saving in db 34 | bcrypt.genSalt(10, (err, salt) => { 35 | bcrypt.hash(newUser.password, salt, (err, hash) => { 36 | if (err) { 37 | throw err; 38 | } 39 | newUser.password = hash; 40 | newUser 41 | .save() 42 | .then((user) => res.json(user)) 43 | .catch((err) => console.log(err)); 44 | }); 45 | }); 46 | } 47 | }); 48 | }); 49 | 50 | // @route POST api/users/login 51 | // @desc Login User and return JWT Token 52 | // @access Public 53 | router.post("/login", (req, res) => { 54 | const { errors, isValid } = validateLoginInput(req.body); 55 | 56 | if (!isValid) { 57 | return res.status(400).json(errors); 58 | } 59 | 60 | const email = req.body.email; 61 | const password = req.body.password; 62 | 63 | User.findOne({ email }).then((user) => { 64 | if (!user) { 65 | return res.status(404).json({ emailNotFound: "Email Not Found" }); 66 | } 67 | // check password 68 | bcrypt.compare(password, user.password).then((isMatch) => { 69 | if (isMatch) { 70 | // Create JWT Payload 71 | const payload = { 72 | id: user.id, 73 | name: user.name, 74 | }; 75 | // Sign Token 76 | jwt.sign( 77 | payload, 78 | secret, 79 | { 80 | expiresIn: 31556926, 81 | }, 82 | (err, token) => { 83 | res.json({ 84 | success: true, 85 | token: token, 86 | }); 87 | } 88 | ); 89 | } else { 90 | return res 91 | .status(400) 92 | .json({ passwordIncorrect: "Password Incorrect" }); 93 | } 94 | }); 95 | }); 96 | }); 97 | 98 | module.exports = router; 99 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const http = require('http') 3 | const socketIO = require('socket.io') 4 | const mongoose = require("mongoose"); 5 | const bodyParser = require("body-parser"); 6 | const passport = require("passport"); 7 | const config = require('config'); 8 | const users = require("./routes/api/users"); 9 | const { remove } = require('./models/User'); 10 | 11 | const port = process.env.PORT || 5000; 12 | 13 | const app = express() 14 | 15 | app.use(bodyParser.urlencoded({ extended: false })); 16 | app.use(bodyParser.json()); 17 | 18 | // const db = require("./config/keys").mongoURI; 19 | const db = config.get('CodeLive.dbConfig.uri'); 20 | 21 | mongoose 22 | .connect(db, { useNewUrlParser: true, useUnifiedTopology: true }) 23 | .then(() => console.log("MongoDB connected")) 24 | .catch((err) => console.log(err)); 25 | 26 | app.use(passport.initialize()); 27 | require("./config/passport")(passport); 28 | app.use("/api/users", users); 29 | 30 | 31 | const server = http.createServer(app) 32 | // Create a socketIO server 33 | const io = socketIO(server, { path: '/sockets' }) 34 | 35 | 36 | var rooms = [] 37 | var removeRooms = [] 38 | 39 | function removingRooms() { 40 | 41 | console.log("ROOMS: " + rooms) 42 | if (removeRooms.length != 0) { 43 | for (let i = 0; i < removeRooms.length; i++) { 44 | if (io.sockets.adapter.rooms[removeRooms[i]] === undefined) { 45 | rooms = rooms.filter(function (item) { 46 | return item !== removeRooms[i] 47 | }) 48 | } 49 | } 50 | } 51 | removeRooms.splice(0,removeRooms.length) 52 | 53 | setTimeout(removingRooms, 60 * 60 * 1000); 54 | } 55 | 56 | 57 | // Triggered whenever a user joins and websocket 58 | // handshake is successfull 59 | io.on("connection", (socket) => { 60 | // ID of the user connected 61 | const { id } = socket.client 62 | console.log(`User connected ${id}`) 63 | 64 | // Check if room exists 65 | socket.on('room-id', msg => { 66 | let exists = rooms.includes(msg) 67 | socket.emit('room-check', exists) 68 | 69 | }) 70 | 71 | // If code changes, broadcast to sockets 72 | socket.on('code-change', msg => { 73 | socket.broadcast.to(socket.room).emit('code-update', msg) 74 | 75 | }) 76 | 77 | // Send initial data to last person who joined 78 | socket.on('user-join', msg => { 79 | let room = io.sockets.adapter.rooms[socket.room] 80 | let lastPerson = Object.keys(room.sockets)[room.length - 1] 81 | io.to(lastPerson).emit('accept-info', msg); 82 | }) 83 | 84 | // Add room to socket 85 | socket.on('join-room', msg => { 86 | console.log("JOINING " + msg) 87 | socket.room = msg 88 | socket.join(msg) 89 | let room = io.sockets.adapter.rooms[socket.room] 90 | if (room.length > 1) { 91 | let user = Object.keys(room.sockets)[0] 92 | io.to(user).emit('request-info', ""); 93 | } 94 | io.sockets.in(socket.room).emit('joined-users', room.length) 95 | }) 96 | 97 | socket.on('created-room', msg => { 98 | console.log("CREATED-ROOM " + msg) 99 | rooms.push(msg) 100 | }) 101 | 102 | 103 | // If language changes, broadcast to sockets 104 | socket.on('language-change', msg => { 105 | io.sockets.in(socket.room).emit('language-update', msg) 106 | }) 107 | 108 | // If title changes, broadcast to sockets 109 | socket.on('title-change', msg => { 110 | io.sockets.in(socket.room).emit('title-update', msg) 111 | }) 112 | 113 | // If connection is lost 114 | socket.on('disconnect', () => { 115 | console.log(`User ${id} disconnected`) 116 | }) 117 | 118 | // Check if there is no one in the room, remove the room if true 119 | socket.on('disconnecting', () => { 120 | try { 121 | let room = io.sockets.adapter.rooms[socket.room] 122 | io.sockets.in(socket.room).emit('joined-users', room.length - 1) 123 | if (room.length === 1) { 124 | console.log("Leaving Room " + socket.room) 125 | socket.leave(socket.room) 126 | removeRooms.push(socket.room) 127 | } 128 | } 129 | catch (error) { 130 | console.log("Disconnect error") 131 | } 132 | }) 133 | }); 134 | 135 | removingRooms() 136 | 137 | server.listen(port, () => console.log(`Listening on port ${port}`)) -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nobitaaah/code-live/accb3a91693d01d8bf0bdba99c666e65a5af7f56/src/App.css -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import { BrowserRouter, Route, Switch, Link } from "react-router-dom"; 3 | import { Provider } from "react-redux"; 4 | import jwt_decode from "jwt-decode"; 5 | import setAuthToken from "./utils/setAuthToken"; 6 | import { setCurrentUser, logoutUser } from "./actions/authActions"; 7 | 8 | 9 | 10 | import { Register, Login, Landing, Navbar, Dashboard, PrivateRoute, Editor, MainPage } from './components' 11 | import store from "./store"; 12 | 13 | import "./App.css"; 14 | 15 | // Check for token to keep user logged in 16 | if (localStorage.jwtToken) { 17 | // Set auth token header auth 18 | const token = localStorage.jwtToken; 19 | setAuthToken(token); 20 | const decoded = jwt_decode(token); 21 | // Set user and isAuthenticated 22 | store.dispatch(setCurrentUser(decoded)); 23 | const currentTime = Date.now() / 1000; // to get in milliseconds 24 | if (decoded.exp < currentTime) { 25 | store.dispatch(logoutUser()); 26 | window.location.href = "./login"; 27 | } 28 | } 29 | 30 | function App() { 31 | 32 | useEffect(() => { 33 | document.title = "CodeLive" 34 | }, []); 35 | 36 | const io = require('socket.io-client') 37 | // URL of server, todo: add dynamic url 38 | const socket = io('http://localhost:5000', { path: '/sockets' }) 39 | // const socket = io('http://192.168.100.35:3000', { path: '/sockets' }) 40 | return ( 41 | 42 | 43 |
44 | {/* */} 45 | {/* */} 46 | 47 | } /> 48 | } /> 49 | 50 | 51 | } /> 52 | 53 |
54 |
55 |
56 | ); 57 | } 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/authActions.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import setAuthToken from "../utils/setAuthToken"; 3 | import jwt_decode from "jwt-decode"; 4 | import { GET_ERRORS, SET_CURRENT_USER, USER_LOADING } from "./types"; 5 | // Register User 6 | export const registerUser = (userData, history) => (dispatch) => { 7 | axios 8 | .post("/api/users/register", userData) 9 | .then((res) => history.push("/login")) // re-direct to login on successful register 10 | .catch((err) => 11 | dispatch({ 12 | type: GET_ERRORS, 13 | payload: err.response.data, 14 | }) 15 | ); 16 | }; 17 | // Login - get user token 18 | export const loginUser = (userData) => (dispatch) => { 19 | axios 20 | .post("/api/users/login", userData) 21 | .then((res) => { 22 | // Save to localStorage 23 | const { token } = res.data; 24 | localStorage.setItem("jwtToken", token); 25 | setAuthToken(token); 26 | const decoded = jwt_decode(token); 27 | dispatch(setCurrentUser(decoded)); 28 | }) 29 | .catch((err) => 30 | dispatch({ 31 | type: GET_ERRORS, 32 | payload: err.response.data, 33 | }) 34 | ); 35 | }; 36 | // Set logged in user 37 | export const setCurrentUser = (decoded) => { 38 | return { 39 | type: SET_CURRENT_USER, 40 | payload: decoded, 41 | }; 42 | }; 43 | // User loading 44 | export const setUserLoading = () => { 45 | return { 46 | type: USER_LOADING, 47 | }; 48 | }; 49 | // Log user out 50 | export const logoutUser = () => (dispatch) => { 51 | localStorage.removeItem("jwtToken"); 52 | setAuthToken(false); 53 | dispatch(setCurrentUser({})); 54 | }; 55 | -------------------------------------------------------------------------------- /src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const GET_ERRORS = "GET_ERRORS"; 2 | export const USER_LOADING = "USER_LOADING"; 3 | export const SET_CURRENT_USER = "SET_CURRENT_USER"; 4 | -------------------------------------------------------------------------------- /src/components/auth/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import PropTypes from "prop-types"; 4 | import { connect } from "react-redux"; 5 | import classnames from "classnames"; 6 | import { loginUser } from "../../actions/authActions"; 7 | 8 | class Login extends Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | email: "", 13 | password: "", 14 | togglePassword: false, 15 | errors: {}, 16 | }; 17 | } 18 | 19 | componentWillReceiveProps(nextProps) { 20 | if (nextProps.auth.isAuthenticated) { 21 | this.props.history.push("/dashboard"); // push user to dashboard when they login 22 | } 23 | if (nextProps.errors) { 24 | this.setState({ 25 | errors: nextProps.errors, 26 | }); 27 | } 28 | } 29 | 30 | componentDidMount() { 31 | // If logged in and user navigates to Login page, should redirect them to dashboard 32 | if (this.props.auth.isAuthenticated) { 33 | this.props.history.push("/dashboard"); 34 | } 35 | } 36 | 37 | onChange = (e) => { 38 | this.setState({ [e.target.id]: e.target.value }); 39 | }; 40 | 41 | onClick = (e) => { 42 | this.setState({ togglePassword: !this.state.togglePassword }); 43 | }; 44 | 45 | onSubmit = (e) => { 46 | e.preventDefault(); 47 | const userData = { 48 | email: this.state.email, 49 | password: this.state.password, 50 | }; 51 | this.props.loginUser(userData); 52 | }; 53 | 54 | render() { 55 | const { errors, togglePassword } = this.state; 56 | 57 | return ( 58 |
59 |
60 |
61 | 62 | 65 | 66 |
67 |

68 |
Login
69 |

70 |
71 |
72 | 73 | 83 | 84 | {errors.email} 85 | {errors.emailNotFound} 86 | 87 |
88 |
89 | 90 |
91 | 102 | {togglePassword ? ( 103 | 107 | ) : ( 108 | 112 | )} 113 |
114 | 115 | {errors.password} 116 | {errors.passwordIncorrect} 117 | 118 |
119 | 125 |
126 | 127 |
128 | New to us? Sign Up 129 |
130 |
131 |
132 | ); 133 | } 134 | } 135 | 136 | Login.propTypes = { 137 | loginUser: PropTypes.func.isRequired, 138 | auth: PropTypes.object.isRequired, 139 | errors: PropTypes.object.isRequired, 140 | }; 141 | 142 | const mapStateToProps = (state) => ({ 143 | auth: state.auth, 144 | errors: state.errors, 145 | }); 146 | 147 | export default connect(mapStateToProps, { loginUser })(Login); 148 | -------------------------------------------------------------------------------- /src/components/auth/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link, withRouter } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | import classnames from "classnames"; 6 | import { registerUser } from "../../actions/authActions"; 7 | 8 | class Register extends Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | name: "", 13 | email: "", 14 | password: "", 15 | password2: "", 16 | togglePassword: false, 17 | togglePassword2: false, 18 | errors: {}, 19 | }; 20 | } 21 | 22 | componentWillReceiveProps(nextProps) { 23 | if (nextProps.errors) { 24 | this.setState({ 25 | errors: nextProps.errors, 26 | }); 27 | } 28 | } 29 | 30 | componentDidMount() { 31 | // If logged in and user navigates to Register page, should redirect them to dashboard 32 | if (this.props.auth.isAuthenticated) { 33 | this.props.history.push("/dashboard"); 34 | } 35 | } 36 | 37 | onChange = (e) => { 38 | this.setState({ [e.target.id]: e.target.value }); 39 | }; 40 | 41 | onClick = (e) => { 42 | this.setState({ 43 | togglePassword: !this.state.togglePassword, 44 | }); 45 | }; 46 | 47 | toggle = (e) => { 48 | this.setState({ togglePassword2: !this.state.togglePassword2 }); 49 | }; 50 | 51 | onSubmit = (e) => { 52 | e.preventDefault(); 53 | const newUser = { 54 | name: this.state.name, 55 | email: this.state.email, 56 | password: this.state.password, 57 | password2: this.state.password2, 58 | }; 59 | this.props.registerUser(newUser, this.props.history); 60 | }; 61 | 62 | render() { 63 | const { errors, togglePassword, togglePassword2 } = this.state; 64 | 65 | return ( 66 |
67 |
68 | 69 | 72 | 73 |

74 |
Sign-Up
75 |

76 |
77 |
78 | 79 | 90 | {errors.name} 91 |
92 |
93 | 94 | 105 | {errors.email} 106 |
107 |
108 | 109 |
110 | 120 | {togglePassword ? ( 121 | 125 | ) : ( 126 | 130 | )} 131 |
132 | {errors.password} 133 |
134 |
135 | 136 |
137 | 147 | {togglePassword2 ? ( 148 | 152 | ) : ( 153 | 157 | )} 158 |
159 | {errors.password2} 160 |
161 | 167 |
168 | 169 |
170 | Already have an account? Login 171 |
172 |
173 |
174 | ); 175 | } 176 | } 177 | 178 | Register.propTypes = { 179 | registerUser: PropTypes.func.isRequired, 180 | auth: PropTypes.object.isRequired, 181 | errors: PropTypes.object.isRequired, 182 | }; 183 | 184 | const mapStateToProps = (state) => ({ 185 | auth: state.auth, 186 | errors: state.errors, 187 | }); 188 | 189 | export default connect(mapStateToProps, { registerUser })(withRouter(Register)); 190 | -------------------------------------------------------------------------------- /src/components/dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | import './dashboard.css' 5 | 6 | import { TimelineLite, TweenMax, Power3 } from 'gsap'; 7 | 8 | import jwt_decode from 'jwt-decode' 9 | 10 | const Dashboard = (props) => { 11 | 12 | const socket = props.socket 13 | const headings = ["Bonjour", "Hola", "Namaste"] 14 | const [currentCount, setCount] = useState(0); 15 | const [render, setRender] = useState(true) 16 | const [name, setName] = useState("") 17 | 18 | const timer = () => setCount(currentCount + 1); 19 | const char_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 20 | const length = 6 21 | const [code, setCode] = useState("") 22 | 23 | 24 | // GSAP ref 25 | let hello = useRef(null) 26 | let helloHidden = useRef(null) 27 | let infoHidden = useRef(null) 28 | let infoHiddenContinue = useRef(null) 29 | let linkInfo = useRef(null) 30 | let button = useRef(null) 31 | let history = useHistory(); 32 | 33 | // New GSAP timeline. 34 | let tl = new TimelineLite({ delay: .4 }); 35 | 36 | if (render == true) { 37 | const token = localStorage.getItem('jwtToken'); 38 | if (token) { 39 | const decoded = jwt_decode(token); 40 | setName(decoded.name) 41 | setRender(false) 42 | } 43 | } 44 | 45 | 46 | // For Hello headings. 47 | useEffect(() => { 48 | if (currentCount > 1) { 49 | return; 50 | } 51 | const id = setInterval(timer, 1000); 52 | return () => clearInterval(id); 53 | }, [currentCount]); 54 | 55 | // Create a code for the room. 56 | const generateCode = () => { 57 | let text = "" 58 | for (let i = 0; i < length; i++) { 59 | text += char_list.charAt(Math.floor(Math.random() * char_list.length)); 60 | } 61 | setCode(text) 62 | } 63 | 64 | // Create a room and redirect user. 65 | useEffect(() => { 66 | if (code !== "") { 67 | if (name === "") { 68 | history.push(`/register`) 69 | } else { 70 | socket.emit('created-room', code) 71 | console.log('CREATED-ROOM') 72 | history.push(`/editor/${code}`) 73 | } 74 | } 75 | 76 | }, [code]) 77 | 78 | useEffect(() => { 79 | 80 | // GSAP animations, no timeline :( 81 | TweenMax.to(hello, 2, { css: { display: 'none' } }) 82 | TweenMax.to(helloHidden, 1, { css: { display: 'inherit' }, delay: 4.5 }) 83 | TweenMax.to(helloHidden, 1, { y: -60, ease: Power3.easeOut, delay: 4.5 }) 84 | TweenMax.to(infoHidden, 3, { css: { display: 'inherit' }, delay: 4.5 }) 85 | TweenMax.to(infoHidden, 1, { y: -40, opacity: 1, ease: Power3.easeInOut, delay: 5.5 }) 86 | TweenMax.to(infoHiddenContinue, 4, { css: { display: 'inherit' }, delay: 4.5 }) 87 | TweenMax.to(infoHiddenContinue, 1, { y: -20, opacity: 1, ease: Power3.easeInOut, delay: 6.5 }) 88 | TweenMax.to(linkInfo, 5, { css: { display: 'inherit' }, delay: 4.5 }) 89 | TweenMax.to(linkInfo, 1, { y: 0, opacity: 1, ease: Power3.easeInOut, delay: 7.5 }) 90 | TweenMax.to(button, 0.5, { opacity: 1, delay: 5.5 }); 91 | 92 | }, [tl]) 93 | 94 | return ( 95 | 96 |
97 |

hello = el} className="heading">{headings[`${currentCount}`]}

98 |

helloHidden = el} className="heading-visible">Hello, {name}

99 |

infoHidden = el}>Your dashboard is pretty empty right now.

100 |

infoHiddenContinue = el}>More features have been planned for this project.

101 |

linkInfo = el}>Have fun.

102 |
button = el}> 103 |
Create a Room
107 |
108 |
109 | ); 110 | } 111 | 112 | export default Dashboard; 113 | 114 | -------------------------------------------------------------------------------- /src/components/dashboard/dashboard.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); 2 | .dashboard { 3 | 4 | display: flex; 5 | height: 100vh; 6 | align-items: center; 7 | justify-content: center; 8 | flex-direction: column; 9 | text-align: center; 10 | } 11 | 12 | .create-room { 13 | padding-top: 5vh; 14 | opacity: 0; 15 | } 16 | 17 | .heading { 18 | font-size: 3.5em; 19 | font-family: 'Poppins', sans-serif; 20 | } 21 | 22 | .heading-visible { 23 | display: none; 24 | font-size: 3.5em; 25 | font-family: 'Poppins', sans-serif; 26 | } 27 | 28 | .intro { 29 | display: none; 30 | opacity: 0; 31 | font-size: 2.5em; 32 | font-family: 'Poppins', sans-serif; 33 | } 34 | 35 | .introContinue { 36 | opacity: 0; 37 | display: none; 38 | font-size: 2.5em; 39 | font-family: 'Poppins', sans-serif; 40 | } 41 | 42 | @media only screen and (max-width: 780px) { 43 | .heading { 44 | font-size: 2.5em; 45 | font-family: 'Poppins', sans-serif; 46 | } 47 | .heading-visible { 48 | display: none; 49 | font-size: 2em; 50 | font-family: 'Poppins', sans-serif; 51 | } 52 | .intro { 53 | display: none; 54 | opacity: 0; 55 | font-size: 2em; 56 | font-family: 'Poppins', sans-serif; 57 | } 58 | .introContinue { 59 | opacity: 0; 60 | display: none; 61 | font-size: 2em; 62 | font-family: 'Poppins', sans-serif; 63 | } 64 | .create-room { 65 | padding-top: 5vh; 66 | } 67 | } -------------------------------------------------------------------------------- /src/components/editor/editor.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap'); 2 | body { 3 | margin: 0; 4 | font-family: 'Poppins', sans-serif; 5 | } 6 | 7 | .listButton-dark { 8 | margin: 0; 9 | background-color: #202124; 10 | position: relative; 11 | } 12 | 13 | .listButton-light { 14 | margin: 0; 15 | background-color: #FFFFFE; 16 | position: relative; 17 | } 18 | 19 | .language-name-dark { 20 | padding-left: 1.5%; 21 | color: white; 22 | } 23 | 24 | .language-name-light { 25 | padding-left: 1.5%; 26 | color: dark; 27 | } 28 | 29 | @media only screen and (max-width: 780px) { 30 | .title-doc { 31 | visibility: hidden; 32 | } 33 | .language-name-dark { 34 | padding-left: 2.5%; 35 | color: white; 36 | } 37 | .language-name-light { 38 | padding-left: 2.5%; 39 | color: dark; 40 | } 41 | } 42 | 43 | .select-dark { 44 | -webkit-appearance: none; 45 | -moz-appearance: none; 46 | -ms-appearance: none; 47 | appearance: none; 48 | outline: 0; 49 | box-shadow: none; 50 | border: 0!important; 51 | background: #5c6664; 52 | background-image: none; 53 | padding: 0 .5em; 54 | margin-left: 1.5%; 55 | color: #fff; 56 | cursor: pointer; 57 | font-size: 1em; 58 | } 59 | 60 | .select-light { 61 | -webkit-appearance: none; 62 | -moz-appearance: none; 63 | -ms-appearance: none; 64 | appearance: none; 65 | outline: 0; 66 | box-shadow: none; 67 | border: 0!important; 68 | background: #f5e9e9; 69 | background-image: none; 70 | padding: 0 .5em; 71 | margin-left: 1.5%; 72 | color: black; 73 | cursor: pointer; 74 | font-size: 1em; 75 | } 76 | 77 | .select::-ms-expand { 78 | display: none; 79 | } 80 | 81 | .title-doc { 82 | display: inline; 83 | position: absolute; 84 | left: 45%; 85 | padding-top: 0.25%; 86 | } 87 | 88 | .input-dark { 89 | margin-right: auto; 90 | margin-left: auto; 91 | -webkit-appearance: none; 92 | -moz-appearance: none; 93 | -ms-appearance: none; 94 | appearance: none; 95 | outline: 0; 96 | box-shadow: none; 97 | border: 0!important; 98 | background: #202124; 99 | background-image: none; 100 | padding: 0 .5em; 101 | margin-left: 0.5%; 102 | color: #fff; 103 | cursor: pointer; 104 | font-size: 1em; 105 | } 106 | 107 | .input-light { 108 | -webkit-appearance: none; 109 | -moz-appearance: none; 110 | -ms-appearance: none; 111 | appearance: none; 112 | outline: 0; 113 | box-shadow: none; 114 | border: 0!important; 115 | background: white; 116 | background-image: none; 117 | padding: 0 .5em; 118 | margin-left: 0.5%; 119 | color: black; 120 | cursor: pointer; 121 | font-size: 1em; 122 | } 123 | 124 | .sunIcon { 125 | color: white; 126 | transform: scale(1.25); 127 | margin-top: 0.25%; 128 | margin-left: 1.5%; 129 | cursor: pointer; 130 | } 131 | 132 | .bulbIcon { 133 | transform: scale(1.25); 134 | margin-left: 1.5%; 135 | margin-top: 0.25%; 136 | cursor: pointer; 137 | } 138 | 139 | .checkIcon { 140 | transform: scale(1); 141 | color: white; 142 | position: absolute; 143 | left: 43.5%; 144 | cursor: pointer; 145 | margin-top: 0.35%; 146 | } 147 | 148 | .logoEditor { 149 | font-size: 1.25em; 150 | margin-left: 1.5%; 151 | } 152 | 153 | .mobile-notValid { 154 | display: flex; 155 | justify-content: center; 156 | align-items: center; 157 | flex-direction: column; 158 | text-align: center; 159 | font-family: 'Poppins', sans-serif; 160 | flex-wrap: wrap; 161 | height: 95vh; 162 | } -------------------------------------------------------------------------------- /src/components/editor/editor.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react' 2 | import { Link } from "react-router-dom"; 3 | import { useParams } from "react-router-dom"; 4 | import { 5 | BrowserView, 6 | MobileView 7 | } from "react-device-detect"; 8 | import './editor.css' 9 | 10 | import { ControlledEditor } from "@monaco-editor/react" 11 | import { FaRegLightbulb } from 'react-icons/fa'; 12 | import { RiSunLine, RiCheckFill } from 'react-icons/ri'; 13 | 14 | 15 | // Code editor 16 | const Editor = (props) => { 17 | 18 | const socket = props.socket 19 | 20 | // Change theme of editor 21 | const [theme, setTheme] = useState("dark") 22 | // Default language JS 23 | const [language, setLanguage] = useState("javascript") 24 | // Check if editor is ready 25 | const [isEditorReady, setIsEditorReady] = useState(false) 26 | // Send chunks of code on change 27 | const [message, setMessage] = useState("") 28 | // Set value of editor 29 | const [value, setValue] = useState('') 30 | const [valid, setValid] = useState(false) 31 | const [sendInitialData, setSendInitialData] = useState(false) 32 | const [users, setUsers] = useState(0) 33 | const [title, setTitle] = useState("Untitled") 34 | const [titleInfo, setTitleInfo] = useState("Untitled") 35 | const [titleChange, setTitleChange] = useState(false) 36 | let { id } = useParams(); 37 | 38 | // Check if room exists 39 | useEffect(() => { 40 | socket.emit('room-id', id) 41 | setValid(true) 42 | }, []) 43 | 44 | // Ref for editor 45 | const editorRef = useRef() 46 | 47 | // Called on initialization, adds ref 48 | const handleEditorDidMount = (_, editor) => { 49 | setIsEditorReady(true); 50 | editorRef.current = editor 51 | } 52 | 53 | // Called whenever there is a change in the editor 54 | const handleEditorChange = (ev, value) => { 55 | // Set value to send over to other sockets 56 | setMessage(value) 57 | }; 58 | 59 | // For theme of code editor 60 | const toggleTheme = () => { 61 | setTheme(theme === "light" ? "dark" : "light") 62 | } 63 | 64 | // If language changes on one socket, emit to all other 65 | useEffect(() => { 66 | socket.emit('language-change', language) 67 | }, [language]) 68 | 69 | 70 | // If there is a code change on a socket, emit to all other 71 | useEffect(() => { 72 | socket.emit('code-change', message) 73 | console.log("CODE-CHANGE: " + message) 74 | }, [message]) 75 | 76 | // If there is a title change on a socket, emit to all other 77 | useEffect(() => { 78 | console.log("Title Updating") 79 | socket.emit('title-change', title) 80 | }, [title]) 81 | 82 | 83 | // Recieve code, title and language changes 84 | useEffect(() => { 85 | socket.on('code-update', (data) => { 86 | console.log("CODE-UPDATE: " + data) 87 | setValue(data) 88 | }) 89 | socket.on('language-update', (data) => { 90 | setLanguage(data) 91 | }) 92 | 93 | socket.on('title-update', (data) => { 94 | setTitleInfo(data) 95 | }) 96 | 97 | socket.on('room-check', (data) => { 98 | if (data === false) { 99 | setValid(false) 100 | } else { 101 | socket.emit('join-room', id) 102 | } 103 | 104 | }) 105 | 106 | socket.on('request-info', (data) => { 107 | setSendInitialData(true) 108 | }) 109 | 110 | // Triggered if new user joins 111 | socket.on('accept-info', (data) => { 112 | console.log(data) 113 | setTitleInfo(data.title) 114 | setLanguage(data.language) 115 | }) 116 | 117 | // Update participants 118 | socket.on('joined-users', (data) => { 119 | setUsers(data) 120 | }) 121 | 122 | }, []) 123 | 124 | 125 | // If a new user join, send him current language and title used by other sockets. 126 | useEffect(() => { 127 | if (sendInitialData == true) { 128 | socket.emit('user-join', { title: title, language: language }) 129 | setSendInitialData(false) 130 | } 131 | }, [sendInitialData]) 132 | 133 | const languages = ["javascript", "python", "c++", "c", "java", "go"] 134 | 135 | const changeLanguage = (e) => { 136 | setLanguage(languages[e.target.value]) 137 | } 138 | 139 | const titleUpdating = (e) => { 140 | setTitleInfo(e.target.value) 141 | setTitleChange(true) 142 | } 143 | 144 | const titleUpdated = (e) => { 145 | setTitle(titleInfo) 146 | setTitleChange(false) 147 | } 148 | 149 | const renderTrue = () => { 150 | return ( 151 | <> 152 | 153 |
154 | 155 |
156 | CodeLive 157 | {theme === "light" && 158 | 159 | 160 | } 161 | {theme !== "light" && 162 | 163 | 164 | } 165 | 166 | 175 | 176 | {language[0].toUpperCase() + language.substr(1)} 177 | Participants: {users} 178 |
179 | 180 |
181 | {titleChange === true && 182 | 183 | } 184 | 185 |
186 | 187 | 196 | 197 |
198 |
199 | 200 |
201 |

Unfortunately, the code editor doesn't work on mobile. There are bugs that we still need to fix before we provide the mobile functionality.

202 |

Kindly use on a Desktop.

203 |
204 |
205 | 206 | ) 207 | } 208 | 209 | const renderFalse = () => { 210 | return ( 211 | <> 212 |

There seems to be no room here.

213 | 214 | ) 215 | } 216 | return ( 217 |
218 | {valid === true 219 | ? renderTrue() 220 | : renderFalse()} 221 |
222 | ); 223 | } 224 | 225 | export default Editor; 226 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export {default as Login } from './auth/Login' 2 | export {default as Register } from './auth/Register' 3 | export {default as Dashboard } from './dashboard/Dashboard' 4 | export {default as Landing } from './layout/Landing' 5 | export {default as Navbar } from './layout/Navbar' 6 | export {default as PrivateRoute} from './private-route/PrivateRoute' 7 | export {default as Editor} from './editor/editor' 8 | export {default as MainPage} from './mainpage/mainpage' -------------------------------------------------------------------------------- /src/components/layout/Landing.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | class Landing extends Component { 5 | render() { 6 | return ( 7 |
8 |

9 | Do whatever you want when you want to. 10 |

11 | 12 |
16 | Sign Up 17 |
18 | 19 | 20 |
21 | Login 22 |
23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | export default Landing; 30 | -------------------------------------------------------------------------------- /src/components/layout/Navbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | class Navbar extends Component { 5 | render() { 6 | return ( 7 |
11 | 12 |

LIVE

13 | 14 |
15 | ); 16 | } 17 | } 18 | export default Navbar; 19 | -------------------------------------------------------------------------------- /src/components/mainpage/codeShare.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nobitaaah/code-live/accb3a91693d01d8bf0bdba99c666e65a5af7f56/src/components/mainpage/codeShare.gif -------------------------------------------------------------------------------- /src/components/mainpage/mainpage.css: -------------------------------------------------------------------------------- 1 | .mainPage { 2 | visibility: hidden; 3 | } 4 | 5 | nav { 6 | width: 100%; 7 | background-color: white; 8 | padding-left: 30px; 9 | padding-right: 10px; 10 | display: flex; 11 | justify-content: space-between; 12 | align-items: center; 13 | } 14 | 15 | .logo { 16 | display: inline-block; 17 | font-size: 1.25em; 18 | } 19 | 20 | .create-room-main { 21 | opacity: 0; 22 | } 23 | 24 | 25 | .headlineThird { 26 | opacity: 0; 27 | } 28 | 29 | 30 | 31 | .nav-links { 32 | list-style: none; 33 | display: flex; 34 | } 35 | 36 | .nav-item{ 37 | margin-right: 1.5vw; 38 | } 39 | 40 | 41 | @media only screen and (max-width: 420px) { 42 | .nav-item{ 43 | margin-right: 2.5vw; 44 | } 45 | 46 | } 47 | 48 | .nav-item a { 49 | display: inline-block; 50 | text-decoration: none; 51 | color: black; 52 | } 53 | 54 | @keyframes strike { 55 | 0% { 56 | transform-origin: 0% 50%; 57 | transform: scaleX(0); 58 | } 59 | 50% { 60 | transform-origin: 0% 50%; 61 | transform: scaleX(1); 62 | } 63 | 50.01% { 64 | transform-origin: 100% 50%; 65 | transform: scaleX(1); 66 | } 67 | 100% { 68 | transform-origin: 100% 50%; 69 | transform: scaleX(0); 70 | } 71 | } 72 | 73 | .linkAnim { 74 | position: relative; 75 | text-decoration: none; 76 | color: black; 77 | } 78 | 79 | .githubIcon { 80 | transform: scale(1.25); 81 | margin-top: 1%; 82 | } 83 | 84 | .linkAnim:hover:after { 85 | content: ' '; 86 | position: absolute; 87 | top: 50%; 88 | left: 0; 89 | width: 100%; 90 | height: 1px; 91 | background: black; 92 | animation: strike 1.25s ease-in-out forwards; 93 | } 94 | 95 | .container-flex { 96 | display: flex; 97 | flex-direction: column; 98 | flex-wrap: wrap; 99 | align-items: center; 100 | justify-content: center; 101 | } 102 | 103 | .container-flex-title { 104 | font-size: 1.75em; 105 | } 106 | 107 | .container-flex-intro { 108 | font-size: 1.25em; 109 | } 110 | 111 | @media only screen and (max-width: 420px) { 112 | .container-flex-title { 113 | font-size: 1.5em; 114 | } 115 | } 116 | 117 | @media only screen and (max-width: 420px) { 118 | .container-flex-intro { 119 | font-size: 1em; 120 | } 121 | } 122 | 123 | .container-flex-info>*+* { 124 | margin-top: 5vh; 125 | } 126 | 127 | .container-flex-info { 128 | text-align: center; 129 | margin-top: 5vh; 130 | } 131 | 132 | .container-flex-intro-continue { 133 | width: 80%; 134 | margin-left: 10%; 135 | margin-right: 10%; 136 | font-size: 1.25em; 137 | margin-top: 2.5vh; 138 | } 139 | 140 | @media only screen and (max-width: 420px) { 141 | .container-flex-intro-continue { 142 | font-size: 1em; 143 | margin-top: 1.5vh; 144 | } 145 | } 146 | 147 | .codeShareGIF { 148 | width: 42.5%; 149 | height: 50%; 150 | max-width: 60%; 151 | } 152 | 153 | @media only screen and (max-width: 780px) { 154 | .codeShareGIF { 155 | width: 80%; 156 | height: 80%; 157 | max-width: 80%; 158 | } 159 | } 160 | 161 | @media only screen and (max-width: 1024px) { 162 | .codeShareGIF { 163 | width: 70%; 164 | height: 80%; 165 | max-width: 80%; 166 | } 167 | } -------------------------------------------------------------------------------- /src/components/mainpage/mainpage.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { 3 | Link, 4 | useHistory 5 | } from "react-router-dom"; 6 | 7 | import './mainpage.css' 8 | import codeShare from './codeShare.gif' 9 | import { TimelineLite, TweenMax, Power3 } from 'gsap'; 10 | 11 | import jwt_decode from 'jwt-decode' 12 | 13 | function MainPage(props) { 14 | 15 | const socket = props.socket 16 | const char_list = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 17 | const length = 6 18 | 19 | const [code, setCode] = useState("") 20 | const [name, setName] = useState("") 21 | const [render, setRender] = useState(true) 22 | 23 | const history = useHistory(); 24 | 25 | let app = useRef(null) 26 | let content = useRef(null) 27 | let image = useRef(null) 28 | let button = useRef(null) 29 | let headlineSecond = useRef(null) 30 | let headlineThird = useRef(null) 31 | let tl = new TimelineLite({ delay: .4 }); 32 | 33 | if (render == true) { 34 | const token = localStorage.getItem('jwtToken'); 35 | if (token) { 36 | const decoded = jwt_decode(token); 37 | setName(decoded.name) 38 | setRender(false) 39 | } 40 | } 41 | 42 | const generateCode = () => { 43 | let text = "" 44 | for (let i = 0; i < length; i++) { 45 | text += char_list.charAt(Math.floor(Math.random() * char_list.length)); 46 | } 47 | setCode(text) 48 | } 49 | 50 | useEffect(() => { 51 | if (code !== "") { 52 | if (name === "") { 53 | history.push(`/register`) 54 | } else { 55 | socket.emit('created-room', code) 56 | console.log('CREATED-ROOM') 57 | history.push(`/editor/${code}`) 58 | } 59 | } 60 | 61 | }, [code]) 62 | 63 | useEffect(() => { 64 | 65 | //content vars 66 | const contentP = content.children[1]; 67 | 68 | //Remove initial flash 69 | TweenMax.to(app, 0, { css: { visibility: 'visible' } }) 70 | // TweenMax.to(button, 5.5, { css: { visibility: 'visible' } }) 71 | TweenMax.to(headlineThird, 0.5, { opacity: 1, delay: 2 }); 72 | TweenMax.to(button, 0.5, { opacity: 1, delay: 2.5 }); 73 | 74 | tl.from(image, 1.6, { x: -1280, ease: Power3.easeOut }, 'Start') 75 | 76 | // Content Animation 77 | tl.staggerFrom([headlineSecond], 1, { 78 | y: 0, 79 | ease: Power3.easeOut, 80 | delay: .2 81 | }, .15, 'Start') 82 | .from(contentP, 1, { y: 40, opacity: 0, ease: Power3.easeOut }, 0.6) 83 | // .from(contentButton, 1, { y: 20, opacity: 0, ease: Power3.easeOut }, 1.6) 84 | 85 | }, [tl]) 86 | 87 | const onLogoutClick = (e) => { 88 | localStorage.removeItem("jwtToken"); 89 | window.location.reload(); 90 | }; 91 | 92 | return ( 93 |
app = el}> 94 | 108 | 109 |
110 |
content = el}> 111 |
112 | Live code sharing made easy. 113 |
114 |
headlineSecond = el}> 115 | Share your code with others for interviews, troubleshooting, teaching & more! 116 |
117 | 118 |
119 | image = el} alt="code share gif" /> 120 |
121 |
headlineThird = el}> 122 | Supports multiple languages with no limit on participants. 123 |
124 |
125 | 126 |
127 |
button = el}> 128 |
132 | Create a Room 133 |
134 |
135 | 136 | 137 |
); 138 | } 139 | 140 | export default MainPage; 141 | -------------------------------------------------------------------------------- /src/components/private-route/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Redirect } from "react-router-dom"; 3 | import { connect } from "react-redux"; 4 | import PropTypes from "prop-types"; 5 | const PrivateRoute = ({ component: Component, auth, ...rest }) => ( 6 | 9 | auth.isAuthenticated === true ? ( 10 | 11 | ) : ( 12 | 13 | ) 14 | } 15 | /> 16 | ); 17 | PrivateRoute.propTypes = { 18 | auth: PropTypes.object.isRequired, 19 | }; 20 | const mapStateToProps = (state) => ({ 21 | auth: state.auth, 22 | }); 23 | export default connect(mapStateToProps)(PrivateRoute); 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | import * as serviceWorker from "./serviceWorker"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /src/reducers/authReducers.js: -------------------------------------------------------------------------------- 1 | import { USER_LOADING, SET_CURRENT_USER } from "../actions/types"; 2 | 3 | const isEmpty = require("is-empty"); 4 | 5 | const initialState = { 6 | isAuthenticated: false, 7 | user: {}, 8 | loading: false, 9 | }; 10 | 11 | export default function (state = initialState, action) { 12 | switch (action.type) { 13 | case SET_CURRENT_USER: 14 | return { 15 | ...state, 16 | isAuthenticated: !isEmpty(action.payload), 17 | user: action.payload, 18 | }; 19 | case USER_LOADING: 20 | return { 21 | ...state, 22 | loading: true, 23 | }; 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/reducers/errorReducers.js: -------------------------------------------------------------------------------- 1 | import { GET_ERRORS } from "../actions/types"; 2 | 3 | const initialState = {}; 4 | 5 | export default function (state = initialState, action) { 6 | switch (action.type) { 7 | case GET_ERRORS: 8 | return action.payload; 9 | default: 10 | return state; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import authReducer from "./authReducers"; 3 | import errorReducer from "./errorReducers"; 4 | 5 | export default combineReducers({ 6 | auth: authReducer, 7 | errors: errorReducer, 8 | }); 9 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import thunk from "redux-thunk"; 3 | import rootReducer from "./reducers"; 4 | 5 | const initialState = {}; 6 | 7 | const middleware = [thunk]; 8 | 9 | const store = createStore( 10 | rootReducer, 11 | initialState, 12 | compose( 13 | applyMiddleware(...middleware), 14 | ) 15 | ); 16 | export default store; 17 | -------------------------------------------------------------------------------- /src/utils/setAuthToken.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const setAuthToken = (token) => { 4 | if (token) { 5 | // Apply auth token toevery req if logged in 6 | axios.defaults.headers.common["Authorization"] = token; 7 | } else { 8 | // Delete auth header 9 | delete axios.defaults.headers.common["Authorization"]; 10 | } 11 | }; 12 | 13 | export default setAuthToken; 14 | -------------------------------------------------------------------------------- /validation/login.js: -------------------------------------------------------------------------------- 1 | const Validator = require("validator"); 2 | const isEmpty = require("is-empty"); 3 | 4 | module.exports = function validateLoginInput(data) { 5 | let errors = {}; 6 | 7 | data.email = !isEmpty(data.email) ? data.email : ""; 8 | data.password = !isEmpty(data.password) ? data.password : ""; 9 | 10 | if (Validator.isEmpty(data.email)) { 11 | errors.email = "Email field is required"; 12 | } else if (!Validator.isEmail(data.email)) { 13 | errors.email = "Email is Invalid"; 14 | } 15 | 16 | if (Validator.isEmpty(data.password)) { 17 | errors.password = "Password field is required"; 18 | } 19 | 20 | return { 21 | errors, 22 | isValid: isEmpty(errors), 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /validation/register.js: -------------------------------------------------------------------------------- 1 | const Validator = require("validator"); 2 | const isEmpty = require("is-empty"); 3 | 4 | module.exports = function validateRegisterInput(data) { 5 | let errors = {}; 6 | 7 | data.name = !isEmpty(data.name) ? data.name : ""; 8 | data.email = !isEmpty(data.email) ? data.email : ""; 9 | data.password = !isEmpty(data.password) ? data.password : ""; 10 | data.password2 = !isEmpty(data.password2) ? data.password2 : ""; 11 | 12 | if (Validator.isEmpty(data.name)) { 13 | errors.name = "Name field required"; 14 | } 15 | 16 | if (Validator.isEmpty(data.email)) { 17 | errors.email = "Email field is required"; 18 | } else if (!Validator.isEmail(data.email)) { 19 | errors.email = "Email is Invalid"; 20 | } 21 | 22 | if (Validator.isEmpty(data.password)) { 23 | errors.password = "Password field is required"; 24 | } 25 | 26 | if (Validator.isEmpty(data.password2)) { 27 | errors.password2 = "Confirm password field is required"; 28 | } 29 | 30 | if (!Validator.isLength(data.password, { min: 8, max: 30 })) { 31 | errors.password = "Password must be atleast 8 character"; 32 | } 33 | 34 | if (!Validator.equals(data.password, data.password2)) { 35 | errors.password2 = "Password must match"; 36 | } 37 | 38 | return { 39 | errors, 40 | isValid: isEmpty(errors), 41 | }; 42 | }; 43 | --------------------------------------------------------------------------------