main
Саске Учиха 2 years ago
parent f13f9bc99b
commit c86523dcf4

@ -10,6 +10,11 @@ Ararat International School - платформа по обучению игры
- Express.js - Express.js
- Mongo DB - Mongo DB
#### V 0.04
- Пункт Мессенджер теперь ведет по пути /messenger
- [Messenger] Добавлен sidebar
- [Messenger] Добавлен список чатов
#### V 0.03 #### V 0.03
- Добавленны UI компоненты: Input, Button, Modal - Добавленны UI компоненты: Input, Button, Modal
- Валидация форм регистрации/авторизации - Валидация форм регистрации/авторизации

@ -25,6 +25,7 @@
"uniqid": "^5.4.0" "uniqid": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.0.2",
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6", "@types/react-dom": "^18.2.6",
@ -871,6 +872,22 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
} }
}, },
"node_modules/@faker-js/faker": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.0.2.tgz",
"integrity": "sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/fakerjs"
}
],
"engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0",
"npm": ">=6.14.13"
}
},
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.10", "version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",

@ -27,6 +27,7 @@
"uniqid": "^5.4.0" "uniqid": "^5.4.0"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.0.2",
"@types/body-scroll-lock": "^3.1.0", "@types/body-scroll-lock": "^3.1.0",
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6", "@types/react-dom": "^18.2.6",

@ -4,6 +4,7 @@ import OnlyUnauthorized from "./components/Auth/OnlyUnauthorized"
import IndexPage from "./pages/IndexPage" import IndexPage from "./pages/IndexPage"
import LoginPage from "./pages/LoginPage" import LoginPage from "./pages/LoginPage"
import RegisterPage from "./pages/RegisterPage" import RegisterPage from "./pages/RegisterPage"
import MessengerPage from "./pages/Messenger/MessengerPage"
import { BrowserRouter, Routes, Route } from 'react-router-dom' import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { checkAuth, userSlice } from "./store/reducers/UserSlice"; import { checkAuth, userSlice } from "./store/reducers/UserSlice";
import { useAppDispatch, useAppSelector } from "./hooks/redux" import { useAppDispatch, useAppSelector } from "./hooks/redux"
@ -34,6 +35,7 @@ function App() {
<Route path='/' element={<RequireAuth><IndexPage/></RequireAuth>}/> <Route path='/' element={<RequireAuth><IndexPage/></RequireAuth>}/>
<Route path='/login' element={<OnlyUnauthorized><LoginPage/></OnlyUnauthorized>}/> <Route path='/login' element={<OnlyUnauthorized><LoginPage/></OnlyUnauthorized>}/>
<Route path='/register' element={<OnlyUnauthorized><RegisterPage/></OnlyUnauthorized>}/> <Route path='/register' element={<OnlyUnauthorized><RegisterPage/></OnlyUnauthorized>}/>
<Route path='/messenger' element={<RequireAuth><MessengerPage/></RequireAuth>}/>
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
) )

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 KiB

@ -16,7 +16,7 @@ const Menu: FC = () => {
<nav> <nav>
<ul className='border-b pb-5'> <ul className='border-b pb-5'>
<MenuItem to='/'>Главная</MenuItem> <MenuItem to='/'>Главная</MenuItem>
<MenuItem to='/1'>Мессенджер</MenuItem> <MenuItem to='/messenger'>Мессенджер</MenuItem>
<MenuItem to='/2'>Программа</MenuItem> <MenuItem to='/2'>Программа</MenuItem>
<MenuItem to='/3'>Онлайн урок</MenuItem> <MenuItem to='/3'>Онлайн урок</MenuItem>
<MenuItem to='/4'>Домашнее задание</MenuItem> <MenuItem to='/4'>Домашнее задание</MenuItem>

@ -11,7 +11,7 @@ const MenuItem: FC<PropsWithChildren<MenuItemProps>> = ({to, children}) => {
const location = useLocation(); const location = useLocation();
return ( return (
<li className={['mb-4', location.pathname === to ? 'relative before:bg-[#fbceb1] before:absolute before:w-[5px] before:h-full before:block before:top-0 before:-left-5': null].join(' ')}><Link to={to} className={['text-slate-500 hover:text-[#fbceb1] transition-all text-lg font-medium', location.pathname === to ? 'text-[#fbceb1]' : null].join(' ')}>{children}</Link></li> <li className={['mb-4', location.pathname === to ? 'relative before:bg-apricot before:absolute before:w-[5px] before:h-full before:block before:top-0 before:-left-5': null].join(' ')}><Link to={to} className={['text-slate-500 hover:text-apricot transition-all text-lg font-medium', location.pathname === to ? 'text-apricot' : null].join(' ')}>{children}</Link></li>
) )
} }

@ -0,0 +1,13 @@
import { FC, PropsWithChildren } from 'react'
import Sidebar from '../Messenger/Sidebar/Sidebar';
const Layout: FC<PropsWithChildren> = ({children}) => {
return (
<main className='flex h-screen'>
<Sidebar/>
{children}
</main>
)
}
export default Layout;

@ -0,0 +1,30 @@
import { FC } from 'react'
import { IChat } from '../../../models/IChat';
import { Link } from 'react-router-dom';
//import { useLocation } from 'react-router-dom';
interface ChatProps {
data: IChat
}
const Chat: FC<ChatProps>= ({data}) => {
// const location = useLocation();
return (
<Link to='/messenger' className={['p-5 flex w-full items-center relative hover:bg-gray-700', ''].join(' ')}>
<div className={['w-12 h-12 min-w-[48px] mr-3 relative', data.isOnline ? 'before:absolute before:w-4 before:h-4 before:bg-green-400 before:rounded-full before:bottom-0 before:right-0 before:border-[3px] before:border-gray-800' : null].join(' ')}>
<img className='rounded-full w-[inherit] h-[inherit]' src={data.avatar} alt="avatar"/>
</div>
<div className="flex flex-col items-start overflow-hidden">
<h2 className='text-white font-medium mb-1'>{data.name + ' ' + data.sname}</h2>
<p className='text-gray-500 text-sm max-w-[200px] whitespace-nowrap overflow-hidden text-ellipsis'>{data.lastmsg.msg}</p>
</div>
<span className="text-sm text-gray-500 absolute right-4 top-4">{data.lastmsg.time}</span>
{data.unreaded > 0 &&
<span className='bg-apricot rounded-full text-sm flex items-center justify-center min-w-[20px] text-gray-800 font-semibold h-5 absolute bottom-[22px] right-4'>{data.unreaded}</span>
}
</Link>
)
}
export default Chat;

@ -0,0 +1,25 @@
import { FC } from 'react'
import Input from '../../UI/Input';
import { BsSearch } from '@react-icons/all-files/bs/BsSearch'
import Chat from './Chat';
import { useAppSelector } from '../../../hooks/redux';
const Chats: FC = () => {
const { chats } = useAppSelector(state => state.MessengerSlice)
return (
<div className='w-80 bg-gray-800 pt-10 pb-5 flex flex-col'>
<div className="px-5 border-b-[1px] border-b-gray-700 pb-5 mb-2">
<h1 className='text-white font-semibold text-2xl mb-4'>Chats</h1>
<Input icon={<BsSearch/>} type='text' className='!py-2' placeholder='Search...'/>
</div>
<div className="flex flex-col overflow-y-auto custom-scroll">
{chats.map(chat=>
<Chat key={chat._id} data={chat}/>
)}
</div>
</div>
)
}
export default Chats;

@ -0,0 +1,28 @@
import { FC } from 'react';
import MenuItem from './MenuItem';
import { BsChatText } from '@react-icons/all-files/bs/BsChatText';
import { BsTelephone } from '@react-icons/all-files/bs/BsTelephone'
import { BsGear } from '@react-icons/all-files/bs/BsGear'
import { useAppSelector } from '../../../hooks/redux';
const Menu: FC = () => {
const { user } = useAppSelector(state=> state.UserSlice);
return (
<nav className='flex flex-col h-full items-center'>
<ul className='border-b border-b-gray-600 pb-8 flex flex-col items-center'>
<MenuItem to='/messenger'><BsChatText/></MenuItem>
<MenuItem className='mb-0' to='/messenger/1'><BsTelephone/></MenuItem>
</ul>
<ul className='pt-8 flex flex-col flex-grow items-center'>
<MenuItem to='/messenger/2'><BsGear/></MenuItem>
</ul>
<div className="w-9 h-9 rounded-full">
<img src={user.avatar} className='w-[inherit] h-[inherit]' alt="avatar" />
</div>
</nav>
)
}
export default Menu;

@ -0,0 +1,19 @@
import { FC, PropsWithChildren } from 'react'
import { Link } from 'react-router-dom';
import { useLocation } from 'react-router-dom';
interface MenuItemProps {
to: string,
className?: string
}
const MenuItem: FC<PropsWithChildren<MenuItemProps>> = ({to, className, children}) => {
const location = useLocation();
return (
<li className={['mb-6', className].join(' ')}><Link to={to} className={['text-white hover:text-apricot transition-all text-xl font-medium', location.pathname === to ? '!text-gray-800 block bg-apricot p-4 rounded-sm' : null].join(' ')}>{children}</Link></li>
)
}
export default MenuItem;

@ -0,0 +1,15 @@
import { FC } from 'react';
import Logo from '../../../assets/logo_sm_white.png';
import { Link } from 'react-router-dom';
import Menu from './Menu';
const Sidebar: FC = () => {
return (
<aside className='w-20 bg-gray-900 py-7 px-2 flex flex-col'>
<Link to='/'><img className='w-14 mb-8' src={Logo} alt="logo"/></Link>
<Menu/>
</aside>
)
}
export default Sidebar;

@ -11,17 +11,21 @@ interface InputProps {
error?: string, error?: string,
placeholder?: string, placeholder?: string,
type: string, type: string,
register?: UseFormRegisterReturn register?: UseFormRegisterReturn,
icon?: React.JSX.Element
} }
const Input: FC<InputProps> = ({className, wrapperClasses, labelClasses, label, name, error, register, type, ...props}) => { const Input: FC<InputProps> = ({className, wrapperClasses, labelClasses, label, name, error, register, type, icon, ...props}) => {
const uid: string = uniqid(); const uid: string = uniqid();
return ( return (
<div className={['flex flex-col relative', wrapperClasses].join(' ')}> <div className={['flex flex-col relative', wrapperClasses].join(' ')}>
{label && {label &&
<label className={['block text-sm text-gray-600 mb-2', labelClasses].join(' ')} htmlFor={uid}>{label}</label> <label className={['block text-sm text-gray-600 mb-2', labelClasses].join(' ')} htmlFor={uid}>{label}</label>
} }
<input type={type} id={uid} name={name} {...register} className={['block w-full px-5 py-3 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-apricot focus:ring-apricot focus:outline-none focus:ring focus:ring-opacity-40', className].join(' ')} {...props} /> {icon &&
<div className="absolute top-1/2 left-3 -translate-y-1/2 text-gray-900">{icon}</div>
}
<input type={type} id={uid} name={name} {...register} className={['block w-full px-5 py-3 text-gray-700 placeholder-gray-400 bg-white border border-gray-200 rounded-md focus:border-apricot focus:ring-apricot focus:outline-none focus:ring focus:ring-opacity-40', icon ? 'pl-9' : null, className].join(' ')} {...props} />
{error && {error &&
<p className="text-red-600 text-sm absolute -bottom-0.5 translate-y-full">{error}</p> <p className="text-red-600 text-sm absolute -bottom-0.5 translate-y-full">{error}</p>
} }

@ -2,6 +2,17 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer components {
.custom-scroll::-webkit-scrollbar {
width: 8px;
}
.custom-scroll::-webkit-scrollbar-thumb {
background-color: white;
border-radius: 20px;
border: 2px solid #1f2937;
}
}
html { html {
font-family: 'Manrope', sans-serif; font-family: 'Manrope', sans-serif;
} }

@ -0,0 +1,11 @@
import { Message } from "./Message";
export interface IChat {
_id: string;
name: string;
sname: string;
avatar: string;
unreaded: number;
lastmsg: Message;
isOnline: boolean;
}

@ -0,0 +1,5 @@
export interface Message {
_id: string;
msg: string;
time: string;
}

@ -0,0 +1,13 @@
import { FC } from 'react'
import Layout from '../../components/Layouts/Messenger';
import Chats from '../../components/Messenger/Chats/Chats';
const MessengerPage: FC = () => {
return (
<Layout>
<Chats/>
</Layout>
)
}
export default MessengerPage;

@ -0,0 +1,39 @@
import { createSlice } from "@reduxjs/toolkit"
import { IChat } from "../../models/IChat";
import { faker } from '@faker-js/faker';
export interface MessengerState {
chats: IChat[];
isLoading: boolean;
}
const initialState: MessengerState = {
chats: [
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 1, isOnline: true, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '23:14'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 4, isOnline: false, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '22:18'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 0, isOnline: true, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '12:10'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 0, isOnline: false, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '16:04'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 0, isOnline: true, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '18:43'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 1, isOnline: true, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '23:14'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 4, isOnline: false, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '22:18'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 0, isOnline: true, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '12:10'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 0, isOnline: false, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '16:04'}},
{_id: faker.string.uuid(), name: faker.person.firstName(), sname: faker.person.lastName(), avatar: faker.internet.avatar(), unreaded: 0, isOnline: true, lastmsg: {_id: faker.string.uuid(), msg: faker.lorem.sentence(), time: '18:43'}}
],
isLoading: true
}
export const messengerSlice = createSlice({
name: 'messengerSlice',
initialState,
reducers: {
storeLoad(state) {
state.isLoading = false;
}
},
})
export default messengerSlice.reducer;

@ -1,8 +1,10 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit"; import { combineReducers, configureStore } from "@reduxjs/toolkit";
import UserSlice from "./reducers/UserSlice"; import UserSlice from "./reducers/UserSlice";
import MessengerSlice from "./reducers/MessengerSlice";
const rootReducer = combineReducers({ const rootReducer = combineReducers({
UserSlice UserSlice,
MessengerSlice
}) })
export const store = () => { export const store = () => {

Loading…
Cancel
Save