Explorar el Código

completed the first version of the project

serg155alternate hace 2 años
padre
commit
ca424171f2

+ 109 - 0
client/package-lock.json

@@ -1345,6 +1345,18 @@
         "stylis": "4.0.13"
       }
     },
+    "@emotion/css": {
+      "version": "11.1.3",
+      "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.1.3.tgz",
+      "integrity": "sha512-RSQP59qtCNTf5NWD6xM08xsQdCZmVYnX/panPYvB6LQAPKQB6GL49Njf0EMbS3CyDtrlWsBcmqBtysFvfWT3rA==",
+      "requires": {
+        "@emotion/babel-plugin": "^11.0.0",
+        "@emotion/cache": "^11.1.3",
+        "@emotion/serialize": "^1.0.0",
+        "@emotion/sheet": "^1.0.0",
+        "@emotion/utils": "^1.0.0"
+      }
+    },
     "@emotion/hash": {
       "version": "0.8.0",
       "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
@@ -4058,6 +4070,11 @@
       "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
       "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
     },
+    "classnames": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz",
+      "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA=="
+    },
     "clean-css": {
       "version": "5.2.4",
       "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.2.4.tgz",
@@ -4093,6 +4110,11 @@
         "wrap-ansi": "^7.0.0"
       }
     },
+    "clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
+    },
     "clone-response": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
@@ -4126,6 +4148,16 @@
       "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
       "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg=="
     },
+    "color": {
+      "version": "0.11.4",
+      "resolved": "https://registry.npmjs.org/color/-/color-0.11.4.tgz",
+      "integrity": "sha1-bXtcdPtl6EHNSHkq0e1eB7kE12Q=",
+      "requires": {
+        "clone": "^1.0.2",
+        "color-convert": "^1.3.0",
+        "color-string": "^0.3.0"
+      }
+    },
     "color-convert": {
       "version": "1.9.3",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -4139,6 +4171,14 @@
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
       "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
     },
+    "color-string": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz",
+      "integrity": "sha1-J9RvtnAlxcL6JZk7+/V55HhBuZE=",
+      "requires": {
+        "color-name": "^1.0.0"
+      }
+    },
     "colord": {
       "version": "2.9.2",
       "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
@@ -6564,6 +6604,11 @@
       "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
       "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA=="
     },
+    "immutable": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
+      "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw=="
+    },
     "import-fresh": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -8501,6 +8546,11 @@
         "tmpl": "1.0.5"
       }
     },
+    "math-random": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/math-random/-/math-random-2.0.1.tgz",
+      "integrity": "sha512-oIEbWiVDxDpl5tIF4S6zYS9JExhh3bun3uLb3YAinHPTlRtW4g1S66LtJrJ4Npq8dgIa8CLK5iPVah5n4n0s2w=="
+    },
     "mdn-data": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
@@ -10121,6 +10171,14 @@
         "performance-now": "^2.1.0"
       }
     },
+    "random-color": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/random-color/-/random-color-1.0.1.tgz",
+      "integrity": "sha1-/P3l0FTu/ti5Co4567Bpc8QCU7E=",
+      "requires": {
+        "color": "^0.11.1"
+      }
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -10129,6 +10187,11 @@
         "safe-buffer": "^5.1.0"
       }
     },
+    "randomcolor": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.6.2.tgz",
+      "integrity": "sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A=="
+    },
     "range-parser": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -10358,6 +10421,37 @@
         "workbox-webpack-plugin": "^6.4.1"
       }
     },
+    "react-scroll-to-bottom": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/react-scroll-to-bottom/-/react-scroll-to-bottom-4.2.0.tgz",
+      "integrity": "sha512-1WweuumQc5JLzeAR81ykRdK/cEv9NlCPEm4vSwOGN1qS2qlpGVTyMgdI8Y7ZmaqRmzYBGV5/xPuJQtekYzQFGg==",
+      "requires": {
+        "@babel/runtime-corejs3": "^7.15.4",
+        "@emotion/css": "11.1.3",
+        "classnames": "2.3.1",
+        "core-js": "3.18.3",
+        "math-random": "2.0.1",
+        "prop-types": "15.7.2",
+        "simple-update-in": "2.2.0"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "3.18.3",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.18.3.tgz",
+          "integrity": "sha512-tReEhtMReZaPFVw7dajMx0vlsz3oOb8ajgPoHVYGxr8ErnZ6PcYEvvmjGmXlfpnxpkYSdOQttjB+MvVbCGfvLw=="
+        },
+        "prop-types": {
+          "version": "15.7.2",
+          "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
+          "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
+          "requires": {
+            "loose-envify": "^1.4.0",
+            "object-assign": "^4.1.1",
+            "react-is": "^16.8.1"
+          }
+        }
+      }
+    },
     "react-transition-group": {
       "version": "4.4.2",
       "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz",
@@ -10770,6 +10864,16 @@
         "sparse-bitfield": "^3.0.3"
       }
     },
+    "sass": {
+      "version": "1.49.8",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.8.tgz",
+      "integrity": "sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw==",
+      "requires": {
+        "chokidar": ">=3.0.0 <4.0.0",
+        "immutable": "^4.0.0",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      }
+    },
     "sass-loader": {
       "version": "12.4.0",
       "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.4.0.tgz",
@@ -11001,6 +11105,11 @@
       "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
       "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
     },
+    "simple-update-in": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/simple-update-in/-/simple-update-in-2.2.0.tgz",
+      "integrity": "sha512-FrW41lLiOs82jKxwq39UrE1HDAHOvirKWk4Nv8tqnFFFknVbTxcHZzDS4vt02qqdU/5+KNsQHWzhKHznDBmrww=="
+    },
     "sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",

+ 4 - 0
client/package.json

@@ -12,9 +12,13 @@
     "express": "^4.17.2",
     "mongoose": "^6.2.1",
     "nodemon": "^2.0.15",
+    "random-color": "^1.0.1",
+    "randomcolor": "^0.6.2",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
     "react-scripts": "5.0.0",
+    "react-scroll-to-bottom": "^4.2.0",
+    "sass": "^1.49.8",
     "socket.io-client": "^4.4.1",
     "web-vitals": "^2.1.4"
   },

+ 18 - 108
client/src/App.js

@@ -1,118 +1,28 @@
 import './App.css';
-import { io } from "socket.io-client";
-import Button from '@mui/material/Button';
-import TextField from '@mui/material/TextField';
-import Container from '@mui/material/Container';
-import Box from '@mui/material/Box';
 import { useState, useEffect } from 'react';
+import { LoginForm } from './components/loginForm/LoginForm';
+import { ChatPage } from './components/chatPage/ChatPage';
 
-const socket = io.connect(process.env.REACT_APP_SERVER_URL || 'http://localhost:5000');
-
-
-socket.on("connect", () => {
-  console.log(socket.id); 
-});
-
-socket.on('connected', (data) => {
-  console.log(data);
-  }).on('error', (e) => {
-  console.log('On connected', e)
-}); 
-
-
-const LoginForm = ({}) => {
+function App() {
 
-    const [userData, setUserdata] = useState({userName:'', password: ''});
     const [token, setToken] = useState(localStorage.getItem('token'))
-    const POST_URL =  process.env.REACT_APP_POST_URL || 'http://localhost:5000/login'
-
-
-    const sendForm = async (POST_URL, userData) => {
-      try {
-          const response = await fetch(POST_URL, {
-              method: 'POST',
-              body: JSON.stringify(userData),
-              headers: {
-                  'Content-Type': 'application/json'
-              }
-          });
-          const json = await response.json();
-          return json;
-  
-      } catch (e) {
-          console.log('Error:', e)
-      }
-  }
 
-    const handleSubmit = async (e) => {
-      e.preventDefault();
-      const data = await sendForm(POST_URL, userData);
-      const token = data.token;
-      console.log(token)
-      setUserdata({userName:'', password: ''});   
-  }
-
-
-
-    useEffect(()=>{
-        return () => {
-            setUserdata({userName:'', password: ''})
+    useEffect(() => {
+        if(token) {
+            localStorage.setItem('token', token);  
+        } 
+    }, [token])
+
+    if (token) {
+        return <ChatPage 
+            token={token} 
+            onExit={() =>{
+                        localStorage.removeItem('token')
+                        setToken('')
+                    }}/> 
         }
-    }, [])
-
 
-    return (
-        <Container maxWidth="xs">
-            <Box
-                component="form" 
-                onSubmit={(e) => handleSubmit(e)}
-                sx={{
-                    marginTop: 40,
-                    display: 'flex',
-                    flexDirection: 'column',
-                }}
-                >
-                <TextField
-                        margin="normal"
-                        required
-                        fullWidth
-                        id="userName"
-                        label="user name"
-                        name="userName"
-                        autoComplete="email"
-                        autoFocus
-                        value={userData.userName}
-                        onChange={e => {
-                            setUserdata({...userData, userName: e.target.value})
-                        }}
-                />
-                <TextField
-                        margin="normal"
-                        required
-                        fullWidth
-                        name="password"
-                        label="Password"
-                        type="password"
-                        id="password"
-                        autoComplete="current-password"
-                        value={userData.password}
-                        onChange={e => setUserdata({...userData, password: e.target.value})}
-                />
-                <Button 
-                    type="submit"
-                    variant="contained"
-                    fullWidth>Login
-                </Button>
-            </Box>
-        </Container>
-    )
-}
-
-
-function App() {
-  return (
-    <LoginForm/>
-  );
+    return <LoginForm onSubmit={setToken}/>; // delete setTokek after unmounted
 }
 
-export default App
+export default App;

+ 266 - 0
client/src/components/chatPage/ChatPage.js

@@ -0,0 +1,266 @@
+import { useEffect, useState, useMemo, useRef, Fragment} from 'react';
+import { MessageForm } from './messageForm/MessageForm';
+import {Button,Avatar, Box, Container} from '@mui/material';
+import { UserInfo } from './userInfo/UserInfo';
+import { dateFormat } from './utils/dateFormat';
+import {io} from 'socket.io-client';
+import './chatPage.scss';
+import { scrollToBottom } from './utils/scrollToBottom';
+import { banUser } from './service/banUser';
+import { muteUser } from './service/muteUser';
+import {sendMessage} from './service/sendMessage';
+
+export const ChatPage = ({ onExit, token }) => {
+
+    const [socket, setSocket] = useState(null);
+    const [messages, setMessages] = useState([])
+    const [user, setUser] = useState({})
+    const [usersOnline, setUsersOnline] = useState([])
+    const [allUsers, setAllUsers] = useState([])
+    const randomColor = require('randomcolor'); 
+    const endMessages = useRef(null);
+    const audio = useRef(null)
+
+    useEffect(() => {
+        if(token){
+            try {
+                setSocket(io.connect( 
+                        process.env.REACT_APP_SERVER_URL || 'http://localhost:5000', 
+                        {auth: {token}})
+                        )
+            } catch (error) {
+                console.log(error)
+            } 
+        }
+    }, [token])
+
+    useEffect(() => {
+
+        if(socket){
+            socket.on('connected', (data) => {
+                setUser(data);
+                }).on('error', (e) => {
+                console.log('On connected', e)
+            }); 
+            socket.on('allmessages', (data) => {
+                    setMessages(data)
+                    }).on('error', (e) => {
+                    console.log('allmessages', e)
+            }); 
+            socket.on('usersOnline', (data) => {
+                setUsersOnline(data)
+                }).on('error', (e) => {
+                console.log(e)
+            });  
+            socket.on('allDbUsers', (data) => {
+                setAllUsers(data);
+                }).on('error', (e) => {
+                console.log(e)
+            }); 
+            socket.on('disconnect', (data) => {
+                if(data === 'io server disconnect') {
+                   socket.disconnect();
+                   onExit(); 
+                } 
+                }).on('error', (e) => {
+                console.log('error token', e)
+            });  
+            socket.on('message', (data) => {
+                setMessages((messages) => [...messages, data] )
+                }).on('error', (e) => {
+                console.log(e)
+            }); 
+             
+        }
+
+        // return () => {
+        //     socket.off('connected');
+        //     socket.off('allmessages');
+        // }
+    }, [socket])
+
+    useEffect(() => {
+        scrollToBottom(endMessages)
+      }, [messages]);
+
+    let userColor = useMemo(() => randomColor(),[]);//color for myavatar
+
+    return (
+        <Container maxWidth="lg">
+            <Box 
+                sx={{
+                    display: 'flex',
+                    height: '100vh'
+                }}>
+                <Box
+                sx={{
+                    display: 'flex',
+                    flexGrow:'2',
+                    flexDirection: 'column',                    
+                }}>
+                    <Box                 
+                    className='messageBox'
+                    sx={{
+                        display: 'flex',
+                        flexGrow:'2',
+                        flexDirection: 'column',
+                        overflow: 'scroll',
+                        height: '90vh'  
+                    }}>                     
+                        {
+                        messages.map((item, i) =>
+                        <Fragment key={i} >
+                            <Avatar 
+                                sx={
+                                    (item.userName == user.userName)
+                                    ? 
+                                    {
+                                        alignSelf: 'flex-end',
+                                        fontSize: 10,
+                                        width: '60px',
+                                        height: '60px',
+                                        color:'black',
+                                        backgroundColor: userColor
+                                    }
+                                    :
+                                    {
+                                        backgroundColor:  (usersOnline.map(current =>{
+                                            if(item.userName == current.userName ) {
+                                                return current.color
+                                            }
+                                          
+                                        } )),
+                                        fontSize: 10,
+                                        width: '60px',
+                                        height: '60px',
+                                        color:'black'
+                                    }
+                                    }> 
+                                    {item.userName}
+                            </Avatar>   
+                            <div 
+                                key={item._id}
+                                className={ 
+                                (item.userName == user.userName)
+                                ? 
+                                'message myMessage' 
+                                :
+                                'message'}
+                                >
+                                    <p>{item.text}</p>  
+                                    <div
+                                     style={{fontStyle:'italic',
+                                            color: 'grey',
+                                            fontSize: 14}}>
+                                            {dateFormat(item).time}
+                                    </div> 
+                                    <div 
+                                    style={{fontStyle:'italic',
+                                            fontSize: 12,
+                                            color: 'grey'}}>
+                                            {dateFormat(item).year}
+                                    </div>
+                            </div>
+                     
+                        </Fragment>
+                        )}
+                        <div ref={endMessages}></div>
+    
+                        </Box>
+                            <MessageForm 
+                                    data = {user} 
+                                    sendMessage = {(data) => {
+                                                    sendMessage(data, socket)
+                                                }}>
+                            </MessageForm>
+                        </Box>
+
+                        <Box
+                        className='usersBox'
+                        sx={{
+                            overflow: 'scroll',  
+                        }}>
+                        <Button 
+                                sx={{margin:'10px 5px'}}
+                                variant="outlined"
+                                onClick={(e)=> {
+                                        socket.disconnect()
+                                        onExit()
+                                        }}>
+                                Logout
+                        </Button>
+
+                        <UserInfo 
+                            data = {user.userName} 
+                            color={userColor}/>
+                            {
+                                user.isAdmin 
+                                ? 
+                                allUsers.map((item) =>
+                                <div 
+                                    key={item._id}
+                                    className='online'>
+                                    <div style={
+                                        {color: (usersOnline.map(current =>{
+                                                if(item.userName == current.userName ) {
+                                                    return current.color
+                                                }
+                                            
+                                            }))}}>{item.userName}</div>
+                                        <div>
+                                            <Button
+                                                variant="contained"
+                                                onClick={()=>{
+                                                    muteUser(item.userName, item.isMutted, socket)
+                                                    }}
+                                                sx={{
+                                                    margin:'3px',
+                                                    height: '25px'
+                                                }}>
+                                                    {item.isMutted
+                                                    ? 
+                                                    'unmute'
+                                                    : 'mute'}
+                                            </Button>
+
+                                            <Button
+                                                variant="contained"
+                                                onClick={()=>{
+                                                    banUser(item.userName, item.isBanned, socket)
+                                                }}
+                                                sx={{
+                                                    margin:'3px',
+                                                    height: '25px'
+                                                }}>
+                                                    {item.isBanned
+                                                ? 'unban'
+                                                : 'ban'}
+                                            </Button>
+
+                                        </div>
+                                {
+                                usersOnline.map((user, i) =>{
+                                                    if(item.userName == user.userName){
+                                                    return <span key={i} style={{color: 'green'}}>online</span>
+                                                    }
+                                                })
+                                }
+                                </div>) 
+                                :
+                                usersOnline.map((item, i) =>
+                                        <div 
+                                            key={i}
+                                            className='online'>  
+                                            <div style={{color: item.color}}>
+                                                {item.userName}
+                                            </div>
+                                            <span style={{color: 'green'}}>
+                                                online
+                                            </span>
+                                        </div>)
+                            }
+                </Box>
+            </Box>
+        </Container>
+    )
+}

+ 48 - 0
client/src/components/chatPage/chatPage.scss

@@ -0,0 +1,48 @@
+
+
+.message {
+    padding: 10px;
+    margin: 5px;
+    max-width: 50%;
+    border-radius: 10px;
+    background-color: rgb(182, 230, 176);
+    p {
+        word-wrap: break-word;
+    }
+    span {
+        background-color:azure;
+        display: block;
+        padding: 5px;
+        height: 20px;
+        border-radius: 20px;
+        text-align: center;
+    }
+}
+
+.myMessage {
+    align-self: flex-end;
+    background-color: rgb(240, 231, 136);
+}
+.messageBox { 
+    display: flex;
+    padding: 20px;
+    border-radius: 10px;
+    background-color:rgb(243, 243, 243)
+}
+.usersBox { 
+    align-content: flex-end;
+    text-align: center;
+    padding: 20px;
+    margin-left: 10px;
+    width: 20%;
+    border-radius: 10px;
+    background-color:rgb(243, 243, 243);
+    .online {
+        border-radius: 5px;
+        padding: 5px;
+        margin-bottom: 5px;
+        background-color:rgb(247, 233, 233);
+        font-weight: 700;
+
+    }
+}

+ 5 - 0
client/src/components/chatPage/message/message.css

@@ -0,0 +1,5 @@
+.message {
+    background-color: beige;
+    width: 200px;
+    height: 500px;
+}

+ 58 - 0
client/src/components/chatPage/messageForm/MessageForm.js

@@ -0,0 +1,58 @@
+import TextareaAutosize from '@mui/material/TextareaAutosize';
+import Button from '@mui/material/Button';
+import { useState } from 'react';
+import Box from '@mui/material/Box';
+
+export const MessageForm = ({sendMessage, data}) => {
+
+    const [message, setMessage] = useState({message: ''});
+
+    return (
+        <Box 
+            component="form" 
+            onSubmit = {(e) =>
+                {
+                    e.preventDefault()
+                    sendMessage(message);
+                    setMessage({message: ''});
+                }}
+                
+                sx={{
+                    display: 'flex',
+                    margin: '20px 5px'
+                }}>
+        
+                    <TextareaAutosize
+                        id="outlined-basic" 
+                        label="Type a message..." 
+                        variant="outlined" 
+                        value={message.message}
+                        placeholder='type you message...'
+                        minRows={3}
+                        maxRows={4}
+                        onKeyPress={(e) => {
+                            if (e.key === "Enter")   {
+                                e.preventDefault();
+                                sendMessage(message);
+                                setMessage({message: ''});
+                            }
+                        }}
+                        onChange={e => setMessage({...message, message: e.target.value})}
+                        style={{
+                            width: '80%',
+                            resize: 'none',
+                            borderRadius: '4px',
+                        }}
+                        /> 
+                    <Button 
+                    variant="contained" 
+                    type='submit'
+                    disabled={data.isMutted}
+                    style={{
+                        width: '20%',
+                    }}
+                    >Send</Button>
+        </Box>            
+    )
+
+}

+ 3 - 0
client/src/components/chatPage/service/banUser.js

@@ -0,0 +1,3 @@
+export const banUser = (user, prevStatus, socket) => {
+    socket.emit('banUser', {user, prevStatus} );
+}

+ 3 - 0
client/src/components/chatPage/service/muteUser.js

@@ -0,0 +1,3 @@
+export const muteUser = (user, prevStatus, socket) => {
+    socket.emit('muteUser', {user, prevStatus} );
+}

+ 5 - 0
client/src/components/chatPage/service/sendMessage.js

@@ -0,0 +1,5 @@
+export const sendMessage = (data, socket) => {
+    if (data.message && data.message.length < 200) {
+        socket.emit('message', data); 
+    } 
+};

+ 14 - 0
client/src/components/chatPage/userInfo/UserInfo.js

@@ -0,0 +1,14 @@
+import Avatar from '@mui/material/Avatar';
+
+export const UserInfo = (data) => {
+    return (
+        <Avatar sx={{ 
+            bgcolor: data.color,
+            width: '100px',
+            height: '100px',
+            fontSize: 14,
+            margin: '20px auto'
+    
+         }}>{data.user}</Avatar>
+    )
+}

+ 9 - 0
client/src/components/chatPage/utils/dateFormat.js

@@ -0,0 +1,9 @@
+export const dateFormat = (item) => {
+
+    const res = item.createDate.split('T');
+    const date = {
+        year : res[0],
+        time : res[1].slice(-13, -5)
+    }
+    return date;
+}  

+ 3 - 0
client/src/components/chatPage/utils/scrollToBottom.js

@@ -0,0 +1,3 @@
+export const scrollToBottom = (endMessages) => {
+    endMessages.current?.scrollIntoView({ behavior: "smooth" })
+}

+ 97 - 0
client/src/components/loginForm/LoginForm.js

@@ -0,0 +1,97 @@
+
+import Button from '@mui/material/Button';
+import TextField from '@mui/material/TextField';
+import Container from '@mui/material/Container';
+import Box from '@mui/material/Box';
+import { useState, useEffect } from 'react';
+import { sendForm } from './utils/sendForm';
+import {isValidPayload} from './utils/validations/isValidPayload';
+import {isValidUserName} from './utils/validations/isValidUserName';
+import { Modal } from '../modal/Modal';
+
+export const LoginForm = ({ onSubmit}) => {
+
+    const [userData, setUserdata] = useState({userName:'', password: ''});
+    const [textModal, setTextModal] = useState('')
+    const [display, setDisplay] = useState('none');
+
+    const POST_URL = 'http://localhost:5000/login' 
+
+    const handleSubmit = async (e) => {
+        e.preventDefault();
+        if(isValidPayload({...userData}) && isValidUserName({...userData})){
+            const data = await sendForm(POST_URL, userData);
+            const token = data.token;
+
+            if(token){
+                onSubmit(token);     
+            }
+
+            setTextModal(data.message)
+            setDisplay('block')
+            setUserdata({userName:'', password: ''});
+        } else {
+            setTextModal('too short or using special symbols')
+            setDisplay('block')
+        }   
+    }
+
+    useEffect(()=>{
+        return () => {
+            setUserdata({userName:'', password: ''})
+        }
+    }, [])
+
+
+    return (
+        <Container maxWidth="xs">
+            <Box
+                component="form" 
+                onSubmit={(e) => handleSubmit(e)}
+                sx={{
+                    marginTop: 40,
+                    display: 'flex',
+                    flexDirection: 'column',
+                }}
+                >
+                <TextField
+                        margin="normal"
+                        required
+                        fullWidth
+                        id="userName"
+                        label="user name"
+                        name="userName"
+                        autoComplete="email"
+                        autoFocus
+                        value={userData.userName}
+                        onChange={e => {
+                            setUserdata({...userData, userName: e.target.value})
+                            setDisplay('none')
+                        }}
+                />
+                <TextField
+                        margin="normal"
+                        required
+                        fullWidth
+                        name="password"
+                        label="Password"
+                        type="password"
+                        id="password"
+                        autoComplete="current-password"
+                        value={userData.password}
+                        onChange={e => setUserdata({...userData, password: e.target.value})}
+                />
+                <Modal 
+                    text={textModal}
+                    propDisplay = {display}
+                
+                ></Modal>
+                <Button 
+                    type="submit"
+                    variant="contained"
+                    fullWidth>Login
+                </Button>
+            </Box>
+        </Container>
+    )
+}

+ 0 - 0
client/src/components/loginForm/utils/dateFormat.js


+ 19 - 0
client/src/components/loginForm/utils/handleSubmit.js

@@ -0,0 +1,19 @@
+export const handleSubmit = async (e, POST_URL, userData) => {
+    e.preventDefault();
+    if(isValidPayload({...userData}) && isValidUserName({...userData})){
+        const data = await sendForm(POST_URL, userData);
+        const token = data.token;
+        if(token){
+            onSubmit(token);     
+        }
+        setTextModal(data.message)
+        setDisplay('block')
+        setUserdata({userName:'', password: ''});
+        
+        
+    } else {
+        setTextModal('too short or using special symbols')
+        setDisplay('block')
+    }
+    
+}

+ 16 - 0
client/src/components/loginForm/utils/sendForm.js

@@ -0,0 +1,16 @@
+export const sendForm = async (POST_URL, userData) => {
+    try {
+        const response = await fetch(POST_URL, {
+            method: 'POST',
+            body: JSON.stringify(userData),
+            headers: {
+                'Content-Type': 'application/json'
+            }
+        });
+        const json = await response.json();
+        return json;
+
+    } catch (e) {
+        console.log('Error:', e)
+    }
+}

+ 3 - 0
client/src/components/loginForm/utils/validations/isValidPayload.js

@@ -0,0 +1,3 @@
+export const isValidPayload = ({userName, password}) => {
+    return (userName.trim().length > 2 && password.trim().length > 4) 
+}

+ 4 - 0
client/src/components/loginForm/utils/validations/isValidUserName.js

@@ -0,0 +1,4 @@
+export const isValidUserName = ({userName}) => {
+    const nameRegex = /[^A-Z a-z0-9]/ ;
+    return !nameRegex.test(userName);
+}

+ 12 - 0
client/src/components/modal/Modal.js

@@ -0,0 +1,12 @@
+import Alert from '@mui/material/Alert';
+
+export const Modal = ({text, propDisplay}) => {
+    return <Alert 
+            severity="error"
+            sx={{
+                display: propDisplay,
+            }}
+            >
+            {text}
+            </Alert>
+}

+ 9 - 0
server/db/models/Message.js

@@ -0,0 +1,9 @@
+const {model, Schema} = require('mongoose');
+
+const Message = new Schema({
+    text: {type: String, required: true},
+    userName : {type: String, required: true},
+    createDate: {type: Date, required: true}
+})
+
+module.exports = model('Message', Message)

+ 6 - 2
server/db/models/User.js

@@ -1,8 +1,12 @@
 const {model, Schema} = require('mongoose');
 
 const User = new Schema({
-    userName: {type: String, unique: true, require: true},
-    hashPassword: {type: String, required: true}
+    userName: {type: String, unique: true, required: true},
+    hashPassword: {type: String, required: true},
+    isAdmin: {type: Boolean, default: false},
+    isBanned: {type: Boolean, default: false},
+    isMutted: {type: Boolean, default: false}
+
 })
 
 module.exports = model('User', User)

+ 172 - 25
server/index.js

@@ -5,15 +5,13 @@ const server = require('http').createServer(app);
 const mongoose = require('mongoose');
 const bcrypt = require('bcrypt');
 const User = require('./db/models/User');
+const Message = require('./db/models/Message');
 const jwt = require('jsonwebtoken');
 
 
-
 const PORT = process.env.PORT || 4000;
-const HASH_KEY = process.env.HASH_KEY;
 const TOKEN_KEY = process.env.TOKEN_KEY || 'rGH452fdfhdfg06hgj7'; 
 
-
 const socket = require('socket.io');
 const cors = require('cors');
 
@@ -21,55 +19,204 @@ app.get('/', (req, res) => {
   res.send('Hello from express server!')
 })
 
+
 app.use(cors());
 app.use(express.json());
 
 const io = socket(server, {
   cors: {
-      origin: "http://localhost:3000" //client endpoint and port
+      origin: "http://localhost:3000"  //client endpoint and port
   }
 });
 
-const generateToken = (id, userName) => {
+const generateToken = (id, userName, isAdmin) => {
   const payload = {
       id,
       userName,
+      isAdmin
   }
   return jwt.sign(payload, TOKEN_KEY);
 }
 
 const getOneUser = async (userName) => {
+  const userInDb = await User.findOne({userName});
   return userInDb;
 }
 
-const user = getOneUser('Sergey');
-
-io.on("connection", async (socket) => {
-  console.log(socket.id); 
-  const userInDb = await User.findOne({userName: 'Sergey'});
-  socket.emit('connected', userInDb);
-});
+const isValidUserName = (userName) => {
+  const nameRegex = /[^A-Za-z0-9]/ ;
+  return (!nameRegex.test(userName) && userName.trim().length > 2);
+}
 
+const getAllDbUsers = async (socket) => {
+  const usersDb = await User.find({})
+  socket.emit('allDbUsers', usersDb) 
+}
 
 
+app.post('/login', async (req, res) => {
+    try {
+        const {userName,password} = req.body;
+        if (!isValidUserName(userName)){
+            return res.status(400).json({message: 'Invalid username'})
+        }
+        const dbUser = await getOneUser(userName)
+        if (dbUser?.isBanned){
+            return res.status(401).json({message: 'Your account has been banned!!!'})
+        }
+        const salt =await bcrypt.genSalt(2);
+        const hashPassword = await bcrypt.hash(password, salt);
+        if (!dbUser) {
+            const user = new User({
+                userName,
+                hashPassword,
+                isAdmin: !await User.count().exec(),
+                isBanned: false,
+                isMutted: false
+            });
+            await user.save()
+            return res.json({
+                token: generateToken(user.id, user.userName, user.isAdmin)
+            });
+        }
+
+        if (dbUser && !bcrypt.compareSync(password, dbUser.hashPassword)){
+            return res.status(400).json({message: 'Invalid credantials'})
+        }
+        res.json({
+            token:  generateToken(dbUser.id, dbUser.userName, dbUser.isAdmin)
+        })
+
+    } catch (e) {
+        console.log(e);
+        res.status(500).json({message: `Error ${e}`});
+    }
+})
 
-const db = async () => {
-  try{
-  const salt = await bcrypt.genSalt(2);
-  const hashPassword = await bcrypt.hash('myPassword', salt);
-  const user = new User({
-    userName: 'Sergey',
-    hashPassword: hashPassword
-  })
-  user.save();
-  }catch(e){
-    console.log(e)
+io.use(async (socket, next) => {
+  const token = socket.handshake.auth.token;
+  const sockets = await io.fetchSockets();
+  if(!token) {
+      socket.disconnect();
+      return;
   }
 
-}
+  const usersOnline = [];
+  sockets.map((sock) => {
+      usersOnline.push(sock.user);
+  }) 
 
-//db();
+ 
+  try {
+      const user = jwt.verify(token, TOKEN_KEY);
+      const userName = user.userName;
+      const dbUser = await getOneUser(userName);
+      if(dbUser.isBanned){
+          socket.disconnect();
+          return;
+      }
+      socket.user = user;
+      const exist = sockets.find((current) => current.user.userName == socket.user.userName)
+
+      if(exist) {  //&& !user.isAdmin  - add for two or more admins 
+          console.log(exist.userName, 'exist twice entering...')   
+          exist.disconnect(); 
+      } 
+  } catch(e) {
+      console.log(e);
+      socket.disconnect();
+  }
+  next();
+});
 
+io.on("connection", async (socket) => {
+  const userName = socket.user.userName;
+  const sockets = await io.fetchSockets();
+  const dbUser = await getOneUser(userName);
+
+  io.emit('usersOnline', sockets.map((sock) => sock.user)); // send array online users  
+  socket.emit('connected', dbUser); //socket.user
+ 
+  if(socket.user.isAdmin){
+       getAllDbUsers(socket); 
+  }//sent all users from db to admin
+
+  const messagesToShow = await Message.find({}).sort({ 'createDate': -1 }).limit(20);
+
+  socket.emit('allmessages', messagesToShow.reverse());
+  socket.on("message", async (data) => {
+      const dateNow = Date.now(); // for correct working latest post 
+      const post = await Message.findOne({userName}).sort({ 'createDate': -1 })
+      const oneUser = await getOneUser(userName);
+      if(oneUser.isMutted){
+          return;
+      } 
+      if(post && ((Date.now() - Date.parse(post?.createDate)) < 5000)){
+          return;
+      }
+      if(!oneUser.isMutted && data){
+      const message = new Message({
+              text: data.message,
+              userName: userName,
+              createDate: Date.now()
+          });
+          try {
+              await message.save(); 
+              console.log('saved to db')
+          } catch (error) {
+              console.log('Message save to db error', error);   
+          }
+          io.emit('message', message);
+      // }
+     } 
+  });
+  
+  try {
+      socket.on("disconnect", async () => {
+          const sockets = await io.fetchSockets();
+          io.emit('usersOnline', sockets.map((sock) => sock.user));
+          console.log(`user :${socket.user.userName} , disconnected to socket`); 
+      });
+          console.log(`user :${socket.user.userName} , connected to socket`); 
+      
+      socket.on("muteUser",async (data) => {
+          if(!socket.user.isAdmin){
+              return;
+          }
+              // if(socket.user.isAdmin){
+                  const {user, prevStatus} = data;
+                  const sockets = await io.fetchSockets();
+                  const mute = await User.updateOne({userName : user}, {$set: {isMutted :!prevStatus}});
+                  getAllDbUsers(socket);
+                  const exist = sockets.find( current => current.user.userName == user)
+                  const dbUser = await getOneUser(user);
+                  
+                  if(exist){
+                      exist.emit('connected', dbUser);   
+                  } 
+              // }
+         });
+  
+      socket.on("banUser",async (data) => {
+          if(!socket.user.isAdmin){
+              return;
+          }
+          // if(socket.user.isAdmin) { 
+              const {user, prevStatus} = data;
+              const sockets = await io.fetchSockets();
+              const ban = await User.updateOne({userName : user}, {$set: {isBanned:!prevStatus}});
+              getAllDbUsers(socket)
+              const exist = sockets.find( current => current.user.userName == user)
+              
+              if(exist){
+                  exist.disconnect();  
+              }
+          // }
+         });
+  } catch (e) {
+      console.log(e);
+  }
+});
 const start = async () => {
   try {
       await mongoose.connect('mongodb://localhost:27017/chat')