DimaBondarenko 2 lat temu
rodzic
commit
55bc0021a4

Plik diff jest za duży
+ 4006 - 31
package-lock.json


+ 12 - 0
package.json

@@ -3,12 +3,24 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "@emotion/react": "^11.7.1",
+    "@emotion/styled": "^11.6.0",
+    "@mui/icons-material": "^5.4.1",
+    "@mui/material": "^5.4.1",
+    "@mui/styled-engine-sc": "^5.4.1",
     "@testing-library/jest-dom": "^5.16.2",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "node-sass": "^7.0.1",
     "react": "^17.0.2",
     "react-dom": "^17.0.2",
+    "react-dropzone": "^12.0.4",
+    "react-redux": "^7.2.6",
+    "react-router-dom": "^5.3.0",
     "react-scripts": "5.0.0",
+    "redux": "^4.1.2",
+    "redux-thunk": "^2.4.1",
+    "styled-components": "^5.3.3",
     "web-vitals": "^2.1.4"
   },
   "scripts": {

+ 3 - 0
public/index.html

@@ -10,6 +10,8 @@
       content="Web site created using create-react-app"
     />
     <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
+    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
     <!--
       manifest.json provides metadata used when your web app is installed on a
       user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -25,6 +27,7 @@
       Learn how to configure a non-root public URL by running `npm run build`.
     -->
     <title>React App</title>
+    <script src='https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.js'></script>
   </head>
   <body>
     <noscript>You need to enable JavaScript to run this app.</noscript>

+ 0 - 38
src/App.css

@@ -1,38 +0,0 @@
-.App {
-  text-align: center;
-}
-
-.App-logo {
-  height: 40vmin;
-  pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
-  .App-logo {
-    animation: App-logo-spin infinite 20s linear;
-  }
-}
-
-.App-header {
-  background-color: #282c34;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  font-size: calc(10px + 2vmin);
-  color: white;
-}
-
-.App-link {
-  color: #61dafb;
-}
-
-@keyframes App-logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}

+ 397 - 18
src/App.js

@@ -1,25 +1,404 @@
-import logo from './logo.svg';
-import './App.css';
+import { applyMiddleware, combineReducers, createStore } from 'redux';
+import thunk from 'redux-thunk';
+import { Provider,connect } from 'react-redux';
+import './App.scss';
+import { backendURL, gql } from './helpers/gql';
+import { useEffect, useState, useRef } from 'react';
+import { authReducer } from './reducers/authReducer';
+import {promiseReducer } from './reducers/promiseReducer';
+import React, { Component } from 'react';
+import {Router, Route, Link,  Navigate, Switch, Redirect} from 'react-router-dom';
+import { createBrowserHistory } from 'history'
+
+
+import LoginForm from './components/LoginForm';
+import { actionAuthLogout } from './actions/actionLogin';
+
+import { actionPromise } from './actions/actionsPromise';
+import { socket } from './actions/actionLogin';
+import { actionAboutMe } from './actions/actionAboutMe';
+import SearchAppBar from './components/AppBar';
+import UserMenu from './components/UserMenu';
+import {Avatar, Grid, List, ListItem} from '@mui/material';
+import { messageReducer } from './reducers/messageReducer';
+import {chatReducer} from './reducers/chatReducer';
+
+// import { uploadFile } from '../helpers/uploadFile';
+
+export const history = createBrowserHistory();
+
+
+
+
+// function DropZMessage({onLoad, nick, url}) {
+// 	const {acceptedFiles, getRootProps, getInputProps} = useDropzone();
+
+// 	useEffect(()=>{
+// 		console.log(acceptedFiles)
+// 		acceptedFiles[0] && onLoad(acceptedFiles[0])
+		
+// 	}, [acceptedFiles])
+
+// 	return (
+// 		<div className="DropZMessage">
+// 			<div {...getRootProps({className: 'dropzone'})}>
+// 				<input {...getInputProps()} />
+// 			</div>
+// 		</div>
+// 	);
+//   }
+  
+//   <DropZ />
+
+
+
+export const actionAddChats = (data) => ({type: 'CHATS', data})
+const actionAddChat = (chat) => ({type: 'CHATS', data: [chat]})
+
+const actionAddMessages = (data, id) => ({type: 'MSG', data, id});
+const actionAddMessage = (message, id) => ({type: 'MSG', data: [message], id});
+const actionClearMessage = (data) => ({type: 'CLEARMSG', data})
+
+const actionAddLastMessage = (chat) => ({type: 'LASTMSG', data: [chat]})
+
+const actionLeftChat = (data) => ({type: 'LEFTCHAT', data})
+const actionFindChat = () => 
+actionPromise('Chat', gql(`query FindChat($q: String) {
+	MessageFind(query: $q) {
+	  _id  text
+	  
+	}
+  }`, {q: JSON.stringify([{}])}))
+
+const actionCreateChat = () => 
+actionPromise('newChat', gql(`mutation createChat($chat: ChatInput) {
+	ChatUpsert(chat: $chat) {
+	  _id
+	  members {
+		_id
+		login
+	  }
+	}
+  }`, ))
+
+
+// const actionFindChatsByUserId = (_id) => 
+// actionPromise('chatsByUserId', gql(`query findUserOne($q1: String) {
+// 	UserFindOne(query: $q1) {
+// 	  nick
+// 	  login
+// 	  _id
+// 	  chats {
+// 		_id
+// 		lastMessage {
+// 			text
+// 			createdAt
+// 		  }
+// 	  }
+	  
+// 	}
+//   }`, {q1: JSON.stringify([{_id}])}))
+
+
+// export const actionGetFullInfo = (_id) => 
+//  	async dispatch => {
+// 		 let user = await dispatch(
+// 			 actionFindChatsByUserId(_id)
+// 		 )
+// 		 if(user){
+// 			//  console.log(user.chats)
+// 			 dispatch(actionAddChats([...user.chats]))
+// 		 }
+// 	 }
+
+
+
+export const actionGetMessageForChat = (_id) => 
+	async (dispatch,getState) => {
+		let messages = await dispatch(
+			actionPromise('messages', gql(`query FindMessChat($chat: String) {
+				MessageFind(query: $chat) {
+				  _id
+				  text
+				  createdAt
+				  owner {
+					nick
+					avatar {
+					  url
+					}
+				  }
+				}
+			  }`, {chat: JSON.stringify([{"chat._id": _id}, {sort: [{_id: -1}]}])}))
+		)
+		if(messages){
+			//getState().
+
+			dispatch(actionAddMessages([...messages], _id))
+		}
+	}
+const actionGetOneChat = (_id) => 
+	async dispatch => {
+		let chat = await dispatch(
+			actionPromise('oneChat', gql(`query findChatById($chatId: String) {
+				ChatFindOne(query: $chatId) {
+				  _id
+				  title
+				  lastModified
+				  lastMessage {
+					text
+					createdAt
+				  }
+				}
+			  }`, {chatId: JSON.stringify([{_id}])}))
+		)
+		if(chat){
+			// dispatch(actionAddChat(chat))
+			console.log(chat)
+		}
+	}
+
+const actionSentOrUpdateMSG = (chatId, text, msgId) => 
+	actionPromise('updateMSG', gql(`mutation MessageUpsert($message: MessageInput) {
+		MessageUpsert(message: $message) {
+		  _id
+		  createdAt
+		  text
+		  owner {
+			nick
+			avatar {
+			  url
+			}
+		  }
+		  chat{
+			_id
+		  }
+		}
+	  }`, {
+		  message : {
+			  _id: msgId,
+			  chat: {_id: chatId},
+			  text
+		  }
+	  }))
+
+const store = createStore(combineReducers({promise: promiseReducer, auth: authReducer, chats: chatReducer}), applyMiddleware(thunk))
+console.log(store.getState())
+store.subscribe(() => console.log(store.getState()));
+// store.dispatch(actionFindChat())
+// store.dispatch(actionAuthLogout())
+
+// store.dispatch(actionGetFullInfo("6200314536a19525919c0402")) //////////////полная инфа после логина 
+
+// let newChat = {_id: '620214de36a1952', title: 'newc2', messages: null}
+// let newChat2  = {_id: '620214de36a19523', title: 'newc2', messages: null}
+// store.dispatch(actionAddChat(newChat))
+// store.dispatch(actionAddChat(newChat2))            //один чат с сокета
+
+if (localStorage.authToken) socket.emit('jwt', localStorage.authToken)
+
+socket.on('jwt_ok',   data => console.log(data))
+socket.on('jwt_fail', error => console.log(error))
+socket.on('msg', msg => {console.log(msg); store.dispatch(actionAddMessage(msg, msg.chat._id)); store.dispatch(actionAddChat(msg.chat))})
+socket.on('chat', chat => {console.log(chat); store.dispatch(actionAddChat(chat))})
+socket.on('chat_left', data => {console.log(data); store.dispatch(actionLeftChat(data)); });
+
+// socket.on("connect", () => {
+// 	console.log(socket.id)
+// })
+// socket.disconnect(true)
+// socket.connect()
+
+
+
+
+const Message = ({mes}) => {
+	return (
+		<div className='Message'>
+			<div>{mes.text}</div>
+			<div>{mes.createdAt}</div>
+		</div>
+	)
+}
+
+const WrapperPageChat = ({children}) => {
+	return (
+		<div className='WrapperPageChat'>{children}</div>
+	)
+} 
+
+const InputMessage = ({onclick, chatId}) => {
+
+	const [inputValue, setValue] = useState('')
+
+	return (
+		<div className='InputMessage'>
+			<input value={inputValue} onChange={(e) => setValue(e.target.value)} type="text"/>	
+			<button onClick={() => onclick(chatId, inputValue)}>Отправить</button>
+		</div>
+	)
+}
+
+const CInputMessage = connect(null, {onclick: actionSentOrUpdateMSG})(InputMessage)
+
+const PageChat = ({chats, chatId}) => {
+	
+	// console.log(chats[chatId].messages)
+	const messagesByChat = Object.values(chats[chatId]?.messages || {})
+	return (
+		<div className='PageChat'>
+			{messagesByChat.map((item) => <Message key={item._id} mes={item}/>)}
+		</div>
+	)
+}
+
+const CPageChat = connect(state=>({chats: state.chats || {}}))(PageChat)
+
+
+const ChatGetData = ({match:{params:{_id}}, getData}) => {
+	useEffect(() => {
+		getData(_id)
+	}, [_id])
+
+	return (
+		<WrapperPageChat>
+			
+			<CPageChat chatId={_id}/>
+			<CInputMessage chatId={_id}/>
+		</WrapperPageChat>
+	)
+}
+
+const CChatGetData = connect(null, {getData: actionGetMessageForChat})(ChatGetData)
+
+const ChatItem = ({chat, handleSet, activeElementId}) => {
+	
+	return (
+			<ListItem  className='ChatItem' onClick={() => handleSet(chat._id)}>
+				<Link className='ChatItem-Link' style={{display: 'flex', textDecoration: 'none', padding: '10px', backgroundColor: 'rgb(245, 245, 245)'}}  to={`/main/${chat._id}`}>
+					<div>
+						<Avatar
+							alt={chat.title}
+							// src={`${backendURL}/${chat}`}
+							sx={{ width: 50, height: 50, mr: '20px'}}
+						/>
+					</div>
+					<div>
+						<div >{chat.title}</div>
+						<div>{chat.lastMessage?.text || 'пусто'}</div>
+					</div>
+				</Link> 
+			</ListItem>
+	)
+}
+
+const Chats = ({chats}) => {
+
+	let [stateId, setStateId] = useState(null)
+
+	useEffect(() => {
+		console.log(stateId)
+	},[stateId])
+
+	
+
+	let chatsArr = Object.values(chats)
+	return (
+		<div className='Chats'>
+			<List sx={{p: '0'}}>
+				{chatsArr.map((item, index) => <ChatItem key={item._id} activeElementId={stateId} chat={item} handleSet={setStateId}/>)}
+			</List>
+		</div>
+	)
+}
+
+const CChats = connect(state => ({chats: state.chats || []}))(Chats)
+
+
+const Aside = () => {
+	const [isOpen, setOpen] = useState(false);
+	
+	return (
+		<aside className='Aside'>
+			<SearchAppBar openUserMenu={() => setOpen(true)}/>
+			<CChats/>
+			<UserMenu open={isOpen} closeUserMenu={() => setOpen(false)}/>
+		</aside>
+	)
+}
+
+const BackgroundPage = ({children}) => 
+	<div className='BackgroundPage'>{children}</div> 
+
+const Alert = () => 
+<div>нет чата</div>
+
+const Main = () => {
+	
+useEffect(() => {
+	store.dispatch(actionAboutMe())
+},[])
+
+	return(
+		<main className='Main'>
+			<Grid container columns={12}>
+				<Grid item xs={4} >
+					<Aside/>
+				</Grid>
+				<Grid item xs={8}>
+					<BackgroundPage>
+						<Switch>
+							<Route path="/main/delete" exact component={Alert}/>
+							<Route path="/main/:_id" exact component={CChatGetData}/>
+						</Switch>
+					</BackgroundPage>
+				</Grid>
+			</Grid>
+		</main>
+	)	
+	
+}
+
+const AllRoutes = ({auth}) => {
+	return (
+				<Switch>
+					<Route path="/login" component={LoginForm} />
+					<Route path="/main" component={Main} />
+					{/* <Route exact path="/">{auth ? <Redirect to="/main"/> : <Redirect to="/login" /> }</Route> */}
+				</Switch>
+	)
+}
+
+const CAllRoutes = connect(state => ({auth : state.auth?.payload}))(AllRoutes)
+
+
+
 
 function App() {
+	
   return (
-    <div className="App">
-      <header className="App-header">
-        <img src={logo} className="App-logo" alt="logo" />
-        <p>
-          Edit <code>src/App.js</code> and save to reload.
-        </p>
-        <a
-          className="App-link"
-          href="https://reactjs.org"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          Learn React
-        </a>
-      </header>
-    </div>
+	<Router history={history}>
+		<Provider store={store}>
+			<div className="App">
+				
+				<CAllRoutes />
+			</div>
+			<button onClick={()=> store.dispatch(actionAuthLogout())}>выйти</button>
+			<button >click</button>
+			
+		</Provider>
+ 	</Router>
   );
 }
 
 export default App;
+
+
+
+
+
+
+
+
+
+
+// по промису чатов делать акшончат

+ 56 - 0
src/App.scss

@@ -0,0 +1,56 @@
+.App {
+  text-align: center;
+	
+	// background-color: rgb(201, 201, 201);
+	.LoginForm{
+		width: 400px;
+		margin: 0 auto;
+    padding: 100px;
+	}
+	.Aside{
+		width: 100%;
+		height: calc(100vh - 64px);
+		.Chats{
+			width: 100%;
+			height: 100%;
+			overflow: hidden auto;
+			.ChatItem{
+				padding: 0;
+				&-Link{
+					width: 100%;
+				}
+			}
+			.ChatItem_active{
+				background-color: cornflowerblue;
+			}
+		}
+	}
+	.Main{
+		.WrapperPageChat{
+			height: 100%;
+			.PageChat{
+				color: white;
+				width: 100%;
+				height: 100%;
+				overflow: hidden auto;
+				display: flex;
+				flex-direction: column-reverse;
+			}
+			.InputMessage{
+				height: 100px;
+			}
+		}
+		
+		
+
+		.BackgroundPage{
+			width: 100%;
+			height: 100vh;
+			background: url("img/20518677.jpg") center center/cover no-repeat;
+		}
+	}
+}
+
+
+
+

+ 0 - 8
src/App.test.js

@@ -1,8 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import App from './App';
-
-test('renders learn react link', () => {
-  render(<App />);
-  const linkElement = screen.getByText(/learn react/i);
-  expect(linkElement).toBeInTheDocument();
-});

+ 45 - 0
src/actions/actionAboutMe.jsx

@@ -0,0 +1,45 @@
+import { actionPromise } from "./actionsPromise";
+import { gql } from "../helpers/gql";
+import { actionAddChats } from "../App";
+
+export const actionFindUser = (_id) => (
+    actionPromise('aboutMe', gql(`query findUserOne($q: String) {
+       UserFindOne (query: $q){
+          _id
+          createdAt
+          login
+          nick
+          avatar {
+             _id
+             url
+          }
+          chats {
+             _id
+             title
+             createdAt
+             lastModified
+             lastMessage {
+                text 
+                createdAt
+              }
+          }
+       }     
+    }`, { 
+          q: JSON.stringify([  {_id: _id}  ])
+       }
+    ))
+ )
+
+export const actionAboutMe = () => 
+        async (dispatch, getState) => {
+            const{auth} = getState();
+            const id = auth?.payload?.sub?.id
+            if(id){
+                let user = await dispatch(actionFindUser(id))
+                console.log(user)
+                if (user){
+                    dispatch(actionAddChats([...user.chats]));
+                    
+                }
+            }
+        } 

+ 26 - 0
src/actions/actionLogin.jsx

@@ -0,0 +1,26 @@
+import { actionPromise } from "./actionsPromise";
+import { gql } from "../helpers/gql";
+import { history } from '../App';
+import { actionAboutMe } from "./actionAboutMe";
+
+export const socket = window.io("ws://chat.fs.a-level.com.ua")
+
+
+const actionAuthLogin = (token) => ({type: 'AUTH_LOGIN', token});
+export const actionAuthLogout = () => ({type: 'AUTH_LOGOUT'});
+
+export const actionFullLogin = (log, pass) => 
+async (dispatch) => {
+	let token = await dispatch(
+	  actionPromise('login', gql(`query login($login: String, $password: String) {
+	  login(login: $login, password: $password)
+	  }`, {login: log, password: pass}))
+  )
+  if(token){
+	  socket.emit('jwt', token)
+	  dispatch(actionAuthLogin(token))
+	  history.push("/");
+	  
+  }
+  return token
+}

+ 30 - 0
src/actions/actionsForChats.jsx

@@ -0,0 +1,30 @@
+import { actionPromise } from "./actionsPromise";
+import { gql } from "../helpers/gql";
+
+
+
+export const actionGetAllChats = (userId) => (
+    actionPromise('getAllChats', gql(`query getAll($q: String){
+       ChatFind (query: $q){
+             _id
+             title
+             avatar {
+                _id
+                url
+             }
+             
+             members {
+                _id
+                login
+                avatar {
+                   _id
+                   url
+                }
+             }
+             lastModified
+       }         
+    }`, { 
+          q: JSON.stringify([ { 'members._id': userId } ])
+          }
+    ))
+ )

+ 17 - 0
src/actions/actionsPromise.jsx

@@ -0,0 +1,17 @@
+const actionPending             = name => ({type:'PROMISE',name, status: 'PENDING'})
+const actionFulfilled = (name,payload) => ({type:'PROMISE',name, status: 'FULFILLED', payload})
+const actionRejected  = (name,error)   => ({type:'PROMISE',name, status: 'REJECTED', error})
+export const actionPromise = (name, promise) =>
+	async (dispatch) => {
+	
+	dispatch(actionPending(name))
+	try {
+		let payload = await promise;
+		dispatch(actionFulfilled(name, payload));
+		
+		return payload
+	}
+	catch(error){
+		dispatch(actionRejected(name, error))
+	}
+}

+ 37 - 0
src/components/AppBar.jsx

@@ -0,0 +1,37 @@
+import * as React from 'react';
+import { styled, alpha } from '@mui/material/styles';
+import AppBar from '@mui/material/AppBar';
+import Box from '@mui/material/Box';
+import Toolbar from '@mui/material/Toolbar';
+import IconButton from '@mui/material/IconButton';
+import Typography from '@mui/material/Typography';
+import InputBase from '@mui/material/InputBase';
+import MenuIcon from '@mui/icons-material/Menu';
+import SearchIcon from '@mui/icons-material/Search';
+
+
+
+
+
+export default function SearchAppBar({openUserMenu}) {
+  return (
+    <Box sx={{ flexGrow: 1 }}>
+      <AppBar position="static">
+        <Toolbar>
+          <IconButton
+            onClick={openUserMenu}
+            size="large"
+            edge="start"
+            color="inherit"
+            aria-label="open drawer"
+            sx={{ mr: 2 }}
+          >
+            <MenuIcon />
+          </IconButton>
+          
+          
+        </Toolbar>
+      </AppBar>
+    </Box>
+  );
+}

+ 110 - 0
src/components/LoginForm.jsx

@@ -0,0 +1,110 @@
+import { actionFullLogin } from '../actions/actionLogin';
+
+
+import TextField from '@mui/material/TextField';
+import { IconButton, InputAdornment, Typography  } from '@mui/material';
+import {BrowserRouter as Router, Route, Link, Routes, Navigate} from 'react-router-dom';
+
+import { useState, useEffect, useRef } from 'react';
+import { Visibility, VisibilityOff } from '@mui/icons-material';
+import Button from '@mui/material/Button';
+import { connect } from 'react-redux';
+import { history } from '../App';
+
+
+
+
+
+function LoginForm({onLogin, }){
+
+    const [values, setValues] = useState({
+        login: '', 
+        password: '',
+        showPassword: false, 
+        error: false
+    })
+
+    const [errorLogin, setErrorLogin] = useState(false);
+    const [errorPass, setErrorPass] = useState(false);
+
+
+    const validate = () => {
+        values.login.length < 3 ? setErrorLogin(true) : setErrorLogin(false);
+        values.password.length < 3 ? setErrorPass(true) : setErrorPass(false);
+        values.password.length < 3 || values.login.length < 3 ? setValues({...values, error: true}) : setValues({...values, error: false})
+        
+    }
+
+
+    const handleChange = (prop) => (event) => {
+        setValues({...values, [prop] : event.target.value})
+    }
+
+    const handleClickShowPassword = () => {
+        setValues({
+            ...values,
+            showPassword: !values.showPassword
+        })
+    }
+
+
+    return (
+        <form className='LoginForm' noValidate autoComplete='off'>
+            <Typography sx={{mb: '30px'}} variant='h4'>Вход</Typography>
+			<TextField 
+                error={errorLogin}
+                sx={{width: '100%', mb: '30px'}} 
+                required  
+                label="Логин" 
+                variant="outlined" 
+                helperText="минимум 3" 
+                value={values.login}
+                autoFocus
+                onChange={handleChange('login')}/>
+
+            <TextField
+                error={errorPass}
+                sx={{width: '100%'}}
+                helperText="минимум 3" 
+                required  
+                variant="outlined" 
+                label="Пароль"
+                type={values.showPassword ? 'text' : 'password'}
+                value={values.password}
+                onChange={handleChange('password')}
+                InputProps={{
+                    endAdornment: (
+                        <InputAdornment position="end">
+                            <IconButton
+                                aria-label="toggle password visibility"
+                                onClick={handleClickShowPassword}
+                                onMouseDown={(event) => event.preventDefault()}
+                                edge="end"
+                            >
+                                {values.showPassword ? <VisibilityOff/> : <Visibility/>}
+                            </IconButton>
+                        </InputAdornment>
+                    )
+                }}
+                
+            />
+
+            {/* {values.error && <div>неверный {errorLogin && <span>логин </span>}{errorPass && <span>пароль</span>}</div>} */}
+            <Button 
+            onClick={
+                (e) => {
+                    if (values.password.length >= 3 && values.login.length >= 3){
+                         onLogin(values.login, values.password);
+                    }
+                }
+            } 
+            sx={{width: '200px', mt: '30px'}} 
+            variant="contained">
+                Войти
+            </Button><br/>
+            <Link className='RegisterBtn' to='/register'>Зарегистрироваться</Link>
+		</form>
+    )
+}
+
+export default connect(null, {onLogin: actionFullLogin})(LoginForm)

+ 117 - 0
src/components/UserMenu.jsx

@@ -0,0 +1,117 @@
+import { Accordion, AccordionDetails, AccordionSummary, Avatar, Button, Drawer, IconButton, List, ListItem, ListItemAvatar, Paper, Typography } from '@mui/material';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { connect } from 'react-redux';
+import { Box } from '@mui/system';
+import { backendURL, gql } from '../helpers/gql';
+import { actionPromise } from '../actions/actionsPromise';
+import React from 'react';
+import {useDropzone} from 'react-dropzone';
+import { useEffect} from 'react';
+import { uploadFile } from '../helpers/uploadFile';
+
+
+
+
+const actionUploadFile = (file) => 
+actionPromise('file', uploadFile(file))
+
+const actionUploadFiles = (files) => 
+actionPromise('filesUpload', Promise.all(files.map((file) => uploadFile(file))))
+
+
+
+
+
+const actionAddAvatar = (userId, avatarId) => 
+actionPromise('avatar', gql(`mutation setAvatar($userId: ID, $avatarId: ID){
+  UserUpsert(user:{_id: $userId, avatar: {_id: $avatarId}}){
+      _id, avatar{
+          _id
+      }
+  }
+}`,{userId: userId, avatarId: avatarId}))
+
+
+
+
+
+
+const actionSetAvatar = file => 
+  async (dispatch, getState) => {
+      const userId = getState().auth.payload.sub.id;
+    //   console.log(userId)
+      let data =  await dispatch(actionUploadFile(file));
+      console.log(data._id)
+      if (data._id){
+         await dispatch(actionAddAvatar(userId, data._id))
+      }
+  }
+
+
+    function DropZ({onLoad, nick, url}) {
+        const {acceptedFiles, getRootProps, getInputProps} = useDropzone();
+    
+        useEffect(()=>{
+            console.log(acceptedFiles)
+            acceptedFiles[0] && onLoad(acceptedFiles[0])
+            
+        }, [acceptedFiles])
+    
+        return (
+          <section className="container">
+            <div {...getRootProps({className: 'dropzone'})}>
+                <input {...getInputProps()} />
+                
+                <Avatar
+                    alt={nick}
+                    src={`${backendURL}/${url}`}
+                    sx={{ width: 70, height: 70, mr: '20px'}}
+                />
+            </div>
+            
+          </section>
+        );
+      }
+      
+      <DropZ />
+    const CDropZ = connect(null, {onLoad: actionSetAvatar})(DropZ)
+
+
+
+const UserMenu = ({open, closeUserMenu, userInfo: {nick, avatar} }) => {
+    let url = avatar?.url
+    
+	
+	return (
+		<Drawer 
+			anchor='left'
+			open={open}
+			onClose={closeUserMenu}
+		>
+
+			<Paper sx={{width: '350px'}}>
+			<Accordion>
+				<AccordionSummary
+					expandIcon={<ExpandMoreIcon/>}
+					aria-controls="panel1a-content"
+					id="panel1a-header"
+				>
+					<CDropZ nick={nick} url={url}/>
+					<br/>
+					<Typography variant='h2' component="h4">{nick}</Typography>
+				</AccordionSummary>
+				<AccordionDetails>
+					<List>
+					<ListItem>first</ListItem>
+					<ListItem>first</ListItem>
+					<ListItem>first</ListItem>
+					</List>
+				</AccordionDetails>
+			</Accordion>
+			</Paper>
+			
+		</Drawer>
+	)
+}
+
+export default connect(state => ({userInfo: state.promise?.aboutMe?.payload || {}}))(UserMenu)

+ 19 - 0
src/helpers/gql.js

@@ -0,0 +1,19 @@
+const getGQL = url =>
+(query, variables) => fetch(url, {
+    method: 'POST',
+    headers: {
+        "Content-Type": "application/json",
+        // 'Accept' : 'application/json',
+        ...(localStorage.authToken ? {"Authorization": "Bearer " + localStorage.authToken} : {})
+    },
+    body: JSON.stringify({query, variables})
+}).then(res => res.json())
+    .then(data => {
+        if (data.data){
+            return Object.values(data.data)[0] 
+        } 
+        else throw new Error(JSON.stringify(data.errors))
+    })
+
+export const backendURL = 'http://chat.fs.a-level.com.ua';
+export const gql = getGQL(backendURL + '/graphql');

+ 11 - 0
src/helpers/uploadFile.js

@@ -0,0 +1,11 @@
+export const uploadFile = (file) => {
+    console.log(file)
+    const fd = new FormData;
+    fd.append('media', file)
+
+    return fetch('http://chat.fs.a-level.com.ua/upload', {
+        method: "POST",
+        headers: localStorage.authToken ? {Authorization: 'Bearer ' + localStorage.authToken} : {},
+        body: fd
+        }).then((res) => res.json())
+}

BIN
src/img/1613592027_11-p-fon-dlya-telegram-11.jpg


BIN
src/img/20518677.jpg


BIN
src/img/321cd404a6e906025816992245d958a8.jpg


BIN
src/img/78534572-flat.jpg


BIN
src/img/8f871db726cc745e7f19064655688335.jpg


Plik diff jest za duży
+ 0 - 1
src/logo.svg


+ 30 - 0
src/reducers/authReducer.js

@@ -0,0 +1,30 @@
+const jwtDecode = token => {
+	try{
+		return JSON.parse(atob(token.split('.')[1]));
+		
+	}
+	catch(e){
+		console.log(e.name, e.message);
+	}
+  }
+  
+export function authReducer(state, {type, token}){                                 
+    if (state === undefined){
+        if(localStorage.authToken){
+            type = 'AUTH_LOGIN';
+            token = localStorage.authToken
+        }
+    }
+    if(type === 'AUTH_LOGIN'){
+        let payload = jwtDecode(token);
+        if (payload){
+            localStorage.authToken = token
+                return {token, payload}
+        }       
+    } 
+    if(type === 'AUTH_LOGOUT'){
+        localStorage.removeItem("authToken")
+        return {}
+    } 
+    return state || {}
+}

+ 115 - 0
src/reducers/chatReducer.js

@@ -0,0 +1,115 @@
+export function chatReducer (state={}, {type, data, id}){
+	if(type === 'CHATS'){
+
+
+		let chats                               ////new chats
+
+		for(const value of data){
+			chats = {
+				...chats, 
+				[value._id]:{...value}
+			}
+		}
+
+        let newState
+
+        // if(Object.values(state).length == 1 && Object.values(state)[0].title === 'loading'){
+        //     newState = {
+        //         ...chats, [Object.keys(state)[0]] : {...chats[Object.keys(state)[0]],
+        //              messages:{...chats[Object.keys(state)[0]]?.messages, ...Object.values(state)[0].messages }}
+        //     }
+        // }
+
+        for (let prop in chats){
+            newState = {
+                ...newState, ...state, [prop] : {...state[prop], ...chats[prop]}
+            }
+        }
+
+        // newState = {...state, ...chats}
+        let arr = Object.entries(newState);
+        arr.sort((a,b) => a[1].lastModified > b[1].lastModified ? -1 : 1)
+		return {
+			...Object.fromEntries(arr)
+		}
+
+	}
+
+    if(type === 'MSG'){
+        let newMessages
+        for(const value of data){
+			newMessages = {
+				...newMessages, 
+				[value._id]:{...value}
+			}
+		}
+        // console.log(newState)
+		return {
+			...state, [id] : {...(state[id] || {_id: id, title: "loading"}),
+             messages: Object.fromEntries(Object.entries({...(state[id]?.messages || {}), ...newMessages}).sort((a,b) => a[0] < b[0] ? 1 : -1))}
+		}
+	}
+    
+
+    	
+
+	// if(type === 'CHATS'){
+		
+	// 	return [...state, ...data].sort((a,b) => a.lastModified > b.lastModified ? -1 : 1)
+
+	// }
+
+    // if(type === 'CHATS'){
+	// 	let chats 
+
+	// 	for(const value of data){
+	// 		chats = {
+	// 			...chats, 
+	// 			[value._id]:{...value}
+	// 		}
+	// 	}
+    //     let newState = {...state, ...chats}
+    //     let arr = Object.entries(newState);
+    //     arr.sort((a,b) => a[1].lastModified > b[1].lastModified ? -1 : 1)
+	// 	return {
+	// 		...Object.fromEntries(arr)
+	// 	}
+
+	// }
+
+	if(type === 'LEFTCHAT'){
+        let newState = {...state};
+        let arr = Object.entries(newState);
+
+        return {
+            ...Object.fromEntries(arr.filter((chat) => chat[0] != data._id))
+        }
+	// 	// console.log(data);
+	// 	// console.log(data._id);
+	// 	return [...state].filter((chat) => chat._id !== data._id)
+		 
+	}
+
+	// if (type === 'LASTMSG'){
+	// 	let newState = [...state].filter((item) => item._id != data[0]._id);
+	// 	return [...newState, ...data].sort((a,b) => a.lastModified > b.lastModified ? -1 : 1)
+	// }
+
+	// if(type === 'MSG'){
+	// 	let newState = [...state];
+
+	// 	let currindex = newState.findIndex((item) => item._id === id)
+		
+	// 	const arr = newState.map((obj, index) =>{
+	// 		if(index == currindex){
+	// 			return ({...obj, messages: data})
+	// 		}
+	// 		return obj
+	// 	})
+	// 	return arr
+		
+	
+
+
+	return state 
+}

+ 45 - 0
src/reducers/messageReducer.js

@@ -0,0 +1,45 @@
+ export function messageReducer (state = {}, {type, data, id}){
+	// if(type === 'MSG'){
+	// 	console.log(data);
+	// 	console.log(id);
+	// 	return {
+	// 		...state, [id] : data.sort((a,b) => a.createdAt < b.createdAt ? -1 : 1)
+	// 	}
+		
+	// // 	for(const value of data){
+	// // 		newState = {
+	// // 			...newState, 
+	// // 			[value._id]:{...value}
+	// // 		}
+	// // 	}
+
+	// // 	return {
+	// // 		...state, [id] : {...state[id], messages: {...state[id].messages, ...newState}}
+	// // 	}
+	// }
+
+	if(type === 'MSG'){
+		
+		let messages
+		for(const value of data){
+			messages = {
+				...messages, 
+			[value._id]:{...value}
+			}
+		}
+		let newState = {
+			...state, [id]: {...state[id], ...messages}
+		}
+		let arr = Object.entries(newState[id])
+		arr.sort((a,b) => a[0] < b[0] ? 1 : -1)
+		newState = {...newState, [id] : Object.fromEntries(arr)}
+		return newState
+	}
+	if(type === 'CLEARMSG'){
+		let newState = {...state};
+		delete newState[data._id]
+		return newState
+	}
+
+	return state
+}

+ 9 - 0
src/reducers/promiseReducer.js

@@ -0,0 +1,9 @@
+export function promiseReducer(state={}, {type, name, status, payload, error}){
+	if (type === 'PROMISE'){
+		return {
+		...state,
+		[name]:{status, payload, error}
+		}
+	}
+return state
+}