├── .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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------