浏览代码

add frontend

master
Ermin Bronja 3 年前
父节点
当前提交
9447699677
共有 50 个文件被更改,包括 32107 次插入0 次删除
  1. 23
    0
      Frontend/.gitignore
  2. 二进制
      Frontend/README.md
  3. 30240
    0
      Frontend/package-lock.json
  4. 57
    0
      Frontend/package.json
  5. 二进制
      Frontend/public/favicon.ico
  6. 43
    0
      Frontend/public/index.html
  7. 二进制
      Frontend/public/logo192.png
  8. 二进制
      Frontend/public/logo512.png
  9. 25
    0
      Frontend/public/manifest.json
  10. 3
    0
      Frontend/public/robots.txt
  11. 38
    0
      Frontend/src/App.css
  12. 38
    0
      Frontend/src/App.js
  13. 8
    0
      Frontend/src/App.test.js
  14. 13
    0
      Frontend/src/MainActionContainer.js
  15. 24
    0
      Frontend/src/components/Activity.js
  16. 266
    0
      Frontend/src/components/ChatList.js
  17. 99
    0
      Frontend/src/components/ChatWindow.js
  18. 66
    0
      Frontend/src/components/CustomerRequest/CustomerRequest.js
  19. 28
    0
      Frontend/src/components/CustomerRequest/CustomerRequest.module.css
  20. 89
    0
      Frontend/src/components/LogInForm.js
  21. 17
    0
      Frontend/src/components/MainContainer.js
  22. 17
    0
      Frontend/src/components/MiddleContainer.js
  23. 11
    0
      Frontend/src/components/ProtectedRoute.js
  24. 100
    0
      Frontend/src/components/RegisterForm.js
  25. 45
    0
      Frontend/src/components/Requests/Requests.js
  26. 4
    0
      Frontend/src/components/Requests/Requests.module.css
  27. 74
    0
      Frontend/src/components/SideNavbar.js
  28. 66
    0
      Frontend/src/components/UI/CustomAccordition.js
  29. 53
    0
      Frontend/src/components/UI/Dialog.js
  30. 5
    0
      Frontend/src/config/urls.js
  31. 31
    0
      Frontend/src/contexts/userContext.js
  32. 41
    0
      Frontend/src/dummyData.js
  33. 103
    0
      Frontend/src/index.css
  34. 25
    0
      Frontend/src/index.js
  35. 1
    0
      Frontend/src/logo.svg
  36. 13
    0
      Frontend/src/reportWebVitals.js
  37. 17
    0
      Frontend/src/screens/ChatsScreen.js
  38. 38
    0
      Frontend/src/screens/HomeScreen.js
  39. 13
    0
      Frontend/src/screens/LoginScreen.js
  40. 18
    0
      Frontend/src/screens/MainScreen.js
  41. 13
    0
      Frontend/src/screens/RegisterScreen.js
  42. 17
    0
      Frontend/src/screens/RequestScreen.js
  43. 13
    0
      Frontend/src/services/chatService.js
  44. 22
    0
      Frontend/src/services/requestService.js
  45. 15
    0
      Frontend/src/services/userService.js
  46. 5
    0
      Frontend/src/setupTests.js
  47. 127
    0
      Frontend/src/store/chat-slice.js
  48. 14
    0
      Frontend/src/store/index.js
  49. 113
    0
      Frontend/src/store/request-slice.js
  50. 16
    0
      Frontend/src/store/ui-slice.js

+ 23
- 0
Frontend/.gitignore 查看文件

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

npm-debug.log*
yarn-debug.log*
yarn-error.log*

二进制
Frontend/README.md 查看文件


+ 30240
- 0
Frontend/package-lock.json
文件差异内容过多而无法显示
查看文件


+ 57
- 0
Frontend/package.json 查看文件

@@ -0,0 +1,57 @@
{
"name": "chattemplate",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@microsoft/signalr": "^6.0.6",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.8.7",
"@mui/styled-engine-sc": "^5.8.0",
"@reduxjs/toolkit": "^1.8.3",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"autoprefixer": "10.4.5",
"axios": "^0.27.2",
"bootstrap": "^5.1.3",
"react": "^18.2.0",
"react-bootstrap": "^2.4.0",
"react-dom": "^18.2.0",
"react-icons": "^4.4.0",
"react-redux": "^8.0.2",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"styled-components": "^5.3.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"overrides": {
"autoprefixer": "10.4.5"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

二进制
Frontend/public/favicon.ico 查看文件


+ 43
- 0
Frontend/public/index.html 查看文件

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
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/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.

Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.

To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

二进制
Frontend/public/logo192.png 查看文件


二进制
Frontend/public/logo512.png 查看文件


+ 25
- 0
Frontend/public/manifest.json 查看文件

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

+ 3
- 0
Frontend/public/robots.txt 查看文件

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

+ 38
- 0
Frontend/src/App.css 查看文件

@@ -0,0 +1,38 @@
.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);
}
}

+ 38
- 0
Frontend/src/App.js 查看文件

@@ -0,0 +1,38 @@
import './App.css';
import {Route, Routes } from 'react-router-dom';
import LoginScreen from './screens/LoginScreen';
import RegisterScreen from './screens/RegisterScreen';
import HomeScreen from './screens/HomeScreen';
import MainScreen from './screens/MainScreen';
import { UserProvider } from './contexts/userContext';
import ProtectedRoute from './components/ProtectedRoute';
import ChatsScreen from './screens/ChatsScreen';
import RequestScreen from './screens/RequestScreen';

function App() {
return (
<div className="App">
<UserProvider>
<Routes>
<Route exact path="/" element={<HomeScreen />} />
<Route path="/login" element={<LoginScreen />} />
<Route path="/register" element={<RegisterScreen />} />
<Route exact path="/main" element={<ProtectedRoute linkToNavigate="/" />}>
<Route exact path="/main" element={<MainScreen />} />
</Route>
<Route exact path="/chats" element={<ProtectedRoute linkToNavigate="/" />}>
<Route exact path="/chats" element={<ChatsScreen />} />
</Route>
<Route exact path="/requests" element={<ProtectedRoute linkToNavigate="/" />}>
<Route exact path="/requests" element={<RequestScreen />} />
</Route>
<Route exact path="/profile" element={<ProtectedRoute linkToNavigate="/" />}>
<Route exact path="/profile" element={<MainScreen />} />
</Route>
</Routes>
</UserProvider>
</div>
);
}

export default App;

+ 8
- 0
Frontend/src/App.test.js 查看文件

@@ -0,0 +1,8 @@
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();
});

+ 13
- 0
Frontend/src/MainActionContainer.js 查看文件

@@ -0,0 +1,13 @@
import React from 'react'

const MainActionContainer = () => {
return (
<div className='min-vh-100'>
<div className='w-100 h-100'>
</div>
</div>
)
}

export default MainActionContainer

+ 24
- 0
Frontend/src/components/Activity.js 查看文件

@@ -0,0 +1,24 @@
import React from 'react'
import { dummyFeed } from '../dummyData'

// Ovde ce biti aktivnosti, notifikacije i slicno...
const Activity = () => {
return (
<div>
<div className='bg-light w-100 mb-3 border-bottom '>
<h3 className='p-0 m-0 py-3 fw-light text-muted'>Feed</h3>
</div>
{dummyFeed.map(n =>
<div
className='pb pt-3 px-3 border-bottom'
key={n.feedId}
>
<h5 className='p-0 m-0 text-start fw-light'>{n.feedName}</h5>
<p className='small p-0 m-0 text-end'>{n.since}</p>
</div>
)}
</div>
)
}

export default Activity

+ 266
- 0
Frontend/src/components/ChatList.js 查看文件

@@ -0,0 +1,266 @@
import React, { useState, useEffect, useContext } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Form, InputGroup } from "react-bootstrap";
import { UserContext } from "../contexts/userContext";
import { FiPlus } from "react-icons/fi";
import { FaSignInAlt } from "react-icons/fa";
import { GiSandsOfTime } from "react-icons/gi";
import { CgEnter } from "react-icons/cg";
import Dialog from "./UI/Dialog";
import { chatActions, fetchChatRoomsAsync } from "../store/chat-slice";
import { createChatRoomAsync } from "../store/chat-slice";
import { createJoinRequestAsync, requestActions } from "../store/request-slice";
import { fetchRequestsAsync } from "../store/request-slice";
import { HubConnectionBuilder } from "@microsoft/signalr";

// Ovde ce biti dostupne grupe i razgovori
const ChatList = () => {
const [createChat, setCreateChat] = useState(false);
// naziv chata koji ce biti dodan
const [chatName, setChatName] = useState("");
// grupe kojima je korisnik poslao zahtev za ulazak
const [requestedRooms, setRequestedRooms] = useState([]);
// chats from redux
const { rooms, status, error } = useSelector((state) => state.chat);
const {
chosenRoom,
status: requestsStatus,
requests,
} = useSelector((state) => state.requests);
// show modal
const [showModal, setShowModal] = useState(false);
const { user } = useContext(UserContext);
// const [connection, setConnection] = useState();
// const [messages, setMessages] = useState([]);
const dispatch = useDispatch();

useEffect(() => {
dispatch(fetchChatRoomsAsync());
dispatch(fetchRequestsAsync());
}, [dispatch]);

useEffect(() => {
if (requests && rooms) {
const userRequests = requests
.filter((request) => request.senderId === user.id)
.map((request) => request.roomId);

setRequestedRooms(userRequests);
}
}, [requests, rooms, user]);

const addChatSubmitHandler = (e) => {
e.preventDefault();
alert(`Chat ${chatName} has been created`);
dispatch(createChatRoomAsync({ name: chatName, createdBy: user.id }));
setCreateChat(false);
setChatName("");
};

const showRoomMessagesHandler = (n) => {
dispatch(chatActions.setRoom(n));
};

const joinRoom = async (n) => {
try {
const connection = new HubConnectionBuilder()
.withUrl("http://localhost:5116/chatHub")
.withAutomaticReconnect()
.build();

connection.on("ReceiveMessage", (data) => {
// When user enter room first time after login, generated Context.ConnectionId will be saved in redux
if (data.connId) {
dispatch(
chatActions.saveContextId({ connId: data.connId, userId: user.id })
);
}
// Every new received message will be stored in redux
dispatch(
chatActions.newMessage({
content: data.message,
createdAtUtc: new Date(),
deletedAtUtc: null,
id: null,
senderId: user.id,
updatedAtUtc: null,
username: user.username,
})
);
});
// When user changed room, array with messages from previous room will be deleted from redux
dispatch(chatActions.newMessage({ changedRoom: true }));

connection.onclose((e) => {
// setConnection();
// setMessages([]);
});

// console.log(n);

await connection.start();
await connection.invoke("JoinRoom", {
userId: user.id,
username: user.username,
roomId: n.id,
});
dispatch(chatActions.setRoom(n));
dispatch(chatActions.setConnection(connection));
// setConnection({ connection });
} catch (e) {
console.log(e);
}
};

const openModal = (n) => {
setShowModal(true);
// console.log(n)
dispatch(requestActions.chooseRoom(n));
};

const dialogHandler = () => {
dispatch(
createJoinRequestAsync({
senderId: user.id,
senderUsername: user.username,
roomId: chosenRoom.id,
roomName: chosenRoom.name,
})
);
};

useEffect(() => {
if (requestsStatus === "idle") {
setShowModal(false);
}
}, [requestsStatus]);

const getView = () => {
let acceptedRequests = [];
let pendingRequests = [];
let availableRequests = [];
for (let i = 0; i < rooms.length; i++) {
if (rooms[i].customers.some((x) => x.customerId === user.id)) {
acceptedRequests.push(rooms[i]);
} else {
if (requestedRooms.includes(rooms[i].id)) {
pendingRequests.push(rooms[i]);
} else {
availableRequests.push(rooms[i]);
}
}
}
return (
<div>
{acceptedRequests.length > 0 && (
<div>
<h1>Accepted</h1>
{acceptedRequests.map((n, index) => (
<div
className="border-bottom d-flex"
key={index}
onClick={() => joinRoom(n)}
>
<button
className="text-start w-100 py-3 px-3 btn btn-light h-100"
onClick={showRoomMessagesHandler.bind(this, n)}
>
{n.name}
</button>
<button className="btn btn-light">
<CgEnter />
</button>
</div>
))}
</div>
)}
{pendingRequests.length > 0 && (
<div>
<h1>Pending</h1>
{pendingRequests.map((n, index) => (
<div className="border-bottom d-flex" key={index}>
<button className="text-start w-100 py-3 px-3 btn btn-light h-100">
{n.name}
</button>
<button className="btn btn-light">
<GiSandsOfTime />
</button>
</div>
))}
</div>
)}
{availableRequests.length > 0 && (
<div>
<h1>Available</h1>
{availableRequests.map((n, index) => (
<div className="border-bottom d-flex" key={index}>
<button className="text-start w-100 py-3 px-3 btn btn-light h-100">
{n.name}
</button>
<button className="btn btn-light" onClick={(e) => openModal(n)}>
<FaSignInAlt />
</button>
</div>
))}
</div>
)}
</div>
);
};

return (
<>
<Dialog
changeModalVisibility={() => setShowModal(false)}
open={showModal}
acceptHandler={dialogHandler}
/>
<div className=" h-100-auto-overflow">
<div className="bg-light w-100 mb-3 border-bottom d-flex justify-content-between align-items-center">
<h3 className="p-0 m-0 py-3 text-center w-50 fw-light text-muted">
Chat
</h3>
{user?.roles[0] === "Support" && (
<button
className="btn btn-light"
onClick={(e) => setCreateChat(true)}
>
<FiPlus />
</button>
)}
</div>
{createChat && (
<div className="w-100 d-flex align-items-center justify-content-center pb-3 border-bottom">
<Form onSubmit={addChatSubmitHandler} className="w-100">
<InputGroup className="w-100 px-2">
<input
type="text"
className="border-1 px-2 w-75"
value={chatName}
onChange={(e) => setChatName(e.target.value)}
/>
<input
type="submit"
className="w-25 btn btn-dark"
value={"Add"}
/>
</InputGroup>
</Form>
</div>
)}

{/* ovo ce biti zamenjeno konkretnim podacima */}
{(!error && requestedRooms && status === "pendingFetchRooms") ||
status === "pendingAddRoom" ? (
<p>Loading</p>
) : (
getView()
)}
{error && <p>REJECTED</p>}
<div className="pt-5"></div>
</div>
</>
);
};

export default ChatList;

+ 99
- 0
Frontend/src/components/ChatWindow.js 查看文件

@@ -0,0 +1,99 @@
import React, { useState, useContext, useEffect, useRef } from "react";
import { Button, Form, FormControl, InputGroup } from "react-bootstrap";
import { UserContext } from "../contexts/userContext";
import { useSelector, useDispatch } from "react-redux";
import { chatActions } from "../store/chat-slice";

const ChatWindow = ({ room }) => {
const messagesEndRef = useRef(null)

const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
}

const [message, setMessage] = useState("");
const { user } = useContext(UserContext);
const connection = useSelector((state) => state.chat.connection);
const connections = useSelector((state) => state.chat.connections);
const activeRoom = useSelector((state) => state.chat.activeRoom);
const messages = useSelector((state) => state.chat.messages);
const dispatch = useDispatch();

useEffect(() => {
dispatch(chatActions.setMessages(room.messages));
}, [dispatch, room.messages]);

useEffect(()=>{
scrollToBottom();
},[])

const onSendMessageToGroupHandler = async () => {
const userToFetch = connections.filter(
(conn) => conn.userId === user.id && conn.roomId === activeRoom.id
);

await connection.invoke("SendMessageToGroup", {
userId: user.id,
message,
connId: userToFetch[0].connId,
});

setMessage('')
};

return (
<div className="px-3 bg-light-transparent rounded h-100 d-flex flex-column">
<div style={{ height: "80px" }}>
<h2 className="py-3 m-0">{room.name}</h2>
</div>

<div className="messages p-3 border d-flex flex-column-reverse">
{/* mapirane poruke */}

{messages.map((n, index) => (
<div
key={index}
className={
n.senderId === user.id
? "d-flex flex-column align-items-end"
: "d-flex flex-column align-items-start"
}
>
<p
className={`p-2 px-4 mb-0 rounded message ${
n.senderId !== user.id ? "bg-primary text-light" : "bg-light text-dark"
}`}
>
{/* {n.message} */}
{n.content}
</p>
<p className="text-muted small m-0 p-0 mb-4">{n.username}</p>
</div>
)).reverse()}
<div ref={messagesEndRef} />
</div>

<Form style={{ height: "80px" }} className="d-flex align-items-center">
<InputGroup className="mb-3">
<FormControl
placeholder="Enter your messagge..."
aria-label="Enter your messagge..."
aria-describedby="basic-addon2"
onChange={(e) => setMessage(e.target.value)}
/>

<Button
className="px-5"
variant="outline-secondary"
id="button-addon2"
onClick={onSendMessageToGroupHandler}
>
Send
</Button>
</InputGroup>
</Form>
</div>
);
};

export default ChatWindow;

+ 66
- 0
Frontend/src/components/CustomerRequest/CustomerRequest.js 查看文件

@@ -0,0 +1,66 @@
import React from "react";
import { FiCheckCircle, FiXCircle } from "react-icons/fi";
import styles from "./CustomerRequest.module.css";
import requestService from "../../services/requestService";
// import { HubConnectionBuilder } from "@microsoft/signalr";

export default function CustomerRequest({ customer, requestHandled }) {
const onAcceptRequestHandler = () => {
requestService
.sendAcceptCustomerRequest({
roomId: customer.roomId,
customerId: customer.senderId,
})
.then((res) => {
requestHandled(res);

// Joining in room
// joinRoom({ userId: customer.senderId, roomId: customer.roomId });
})
.catch((e) => console.log(e));
};

// const joinRoom = async (joiningCredentials) => {
// try {
// const connection = new HubConnectionBuilder()
// .withUrl("http://localhost:5116/chatHub")
// .withAutomaticReconnect()
// .build();

// connection.on("ReceiveMessage", (data) => {
// console.log("CUSTOMER REQUEST", data);
// });

// await connection.start();
// await connection.invoke("JoinRoom", joiningCredentials);
// } catch (e) {
// console.log(e);
// }
// };

const onRejectRequestHandler = () => {
requestService
.rejectCustomerRequest({
roomId: customer.roomId,
customerId: customer.senderId,
})
.then((res) => requestHandled(res))
.catch((e) => console.log(e));
};

return (
<div className={styles.container}>
<p>{customer.senderUsername}</p>
<div className={styles.iconsContainer}>
<FiCheckCircle
className={styles.greenIcon}
onClick={onAcceptRequestHandler}
/>
<FiXCircle
className={styles.redIcon}
onClick={onRejectRequestHandler}
/>
</div>
</div>
);
}

+ 28
- 0
Frontend/src/components/CustomerRequest/CustomerRequest.module.css 查看文件

@@ -0,0 +1,28 @@
.container{
display: flex;
align-items: center;
width: 100%;
justify-content: space-between;
}

.container p{
margin: 0;
}

.iconsContainer{
display: flex;
width: 20%;
justify-content: space-between;
}

.greenIcon{
font-size: 1.5rem;
cursor: pointer;
color: green;
}

.redIcon{
font-size: 1.5rem;
cursor: pointer;
color: red;
}

+ 89
- 0
Frontend/src/components/LogInForm.js 查看文件

@@ -0,0 +1,89 @@
import React, { useContext, useState } from 'react'
import {FloatingLabel, Form } from 'react-bootstrap'
import { Link, useNavigate } from 'react-router-dom'
import userService from '../services/userService'
import { UserContext } from '../contexts/userContext'

const LogInForm = () => {
const navigate = useNavigate()

const { setUser } = useContext(UserContext)

const [username, setUserName] = useState('')
const [password, setPassword] = useState('')

const [err, setError] = useState({
username: false,
password: false
})

function validate(){
let usernameError = false
let passError = false

if(!username){
usernameError = true
}
if(!password){
passError = true
}

if(usernameError || passError){
setError({
username: usernameError,
password: passError
})
return false;
}

setError({
username: false,
password: false
})
return true;
}

const submitHandler = (e) =>{
e.preventDefault();
if(validate())
userService.logIn({
username,
password
})
.then(res => {
localStorage.setItem("user", JSON.stringify(res))
setUser(res)
navigate('/main')
})
.catch(err =>{
console.log(err)
})
}

return (
<Form onSubmit={submitHandler} className='p-3 p-md-5 rounded bg-dark text-light'>
<h5 className='text-start p-0 m-0 display-6'>Sign In</h5>
<p className='text-muted p-0 m-0 py-3 pb-4 text-start'>Welcome back</p>
<FloatingLabel
label="Username"
className="mb-3"
>
<Form.Control value={username} onChange={e => setUserName(e.target.value)} type="text" className={`bg-dark responsive-input text-light ${err.username ? 'border-danger' : ''}`} placeholder="Enter your username..." />
</FloatingLabel>
<FloatingLabel
label="Password"
className="mb-4"
>
<Form.Control value={password} onChange={e => setPassword(e.target.value)} type="password" className={`bg-dark responsive-input text-light ${err.password ? 'border-danger' : ''}`} placeholder="Enter your password..." />
</FloatingLabel>
<Link className='text-light my-3' to='/'>Forgott password? Click here</Link><br></br>
<input type='submit' className='btn py-2 mt-4 w-100 btn-main text-dark' value={'log in'} />
<p className='text-light m-0 p-0 py-3'>or</p>
<p className='text-light m-0 p-0'>No account yet?</p>
<Link className='text-light m-0 p-0' to='/register'>Click here to register</Link><br></br>
</Form>
)
}

export default LogInForm

+ 17
- 0
Frontend/src/components/MainContainer.js 查看文件

@@ -0,0 +1,17 @@
import React from 'react'
import { useSelector } from 'react-redux';
import ChatWindow from './ChatWindow';

const MainContainer = ({showTerm}) => {
const { activeRoom } = useSelector((state) => state.chat);

return (
<div className='h-100-auto-overflow p-3 w-100'>
{showTerm === 'chats' && activeRoom !== null ? <ChatWindow room={activeRoom} /> : ''}
{showTerm === 'empty' && ''}
</div>
)
}

export default MainContainer

+ 17
- 0
Frontend/src/components/MiddleContainer.js 查看文件

@@ -0,0 +1,17 @@
import React from 'react'
import Activity from './Activity'
import ChatList from './ChatList'
import Requests from './Requests/Requests'

const MiddleContainer = ({showTerm}) => {

return (
<div className='w-25 mh-100-vh px-3 bg-light'>
{showTerm === 'chats' && <ChatList />}
{showTerm === 'notifications' && <Activity />}
{showTerm === 'requests' && <Requests/>}
</div>
)
}

export default MiddleContainer

+ 11
- 0
Frontend/src/components/ProtectedRoute.js 查看文件

@@ -0,0 +1,11 @@
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';

const ProtectedRoute = ({ linkToNavigate }) => {
const user = JSON.parse(localStorage.getItem('user'))
return (
user ? <Outlet /> : <Navigate to={linkToNavigate} />
);
};

export default ProtectedRoute;

+ 100
- 0
Frontend/src/components/RegisterForm.js 查看文件

@@ -0,0 +1,100 @@
import React, { useContext, useState } from 'react'
import { Col, FloatingLabel, Form, FormGroup, Row } from 'react-bootstrap'
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../contexts/userContext';
import userService from '../services/userService'

const RegisterForm = () => {

const navigate = useNavigate();
const { setUser } = useContext(UserContext)

const [registerValues, setRegister] = useState({
username: '',
password: '',
email: '',
firstName: '',
lastName: ''
})

const submitHandler = (e) =>{
e.preventDefault();
// potrebna jos validacija

userService.register(registerValues)
.then(res => {
localStorage.setItem("user", JSON.stringify(res))
setUser(res)
navigate('/main')
})
.catch(err => console.log(err))
}

return (
<Form onSubmit={submitHandler} className='p-3 p-md-5 py-3 py-md-4 rounded bg-dark text-light'>
<h5 className='text-start p-0 m-0 display-6'>Sign Up</h5>
<p className='text-muted p-0 m-0 py-3 pb-4 text-start'>Please enter your valid credentials</p>
<FormGroup as={Row}>
<Col md='6' className='pe-1'>
<FloatingLabel
label="First Name"
className="mb-3"
>
<Form.Control value={registerValues.firstName} onChange={e => setRegister({...registerValues, firstName: e.target.value})} type="text" className='bg-dark w-100 text-light' placeholder="Enter your first name..." />
</FloatingLabel>
</Col>
<Col md='6' className='ps-1'>
<FloatingLabel
label="Last Name"
className="mb-3"
>
<Form.Control value={registerValues.lastName} onChange={e => setRegister({...registerValues, lastName: e.target.value})} type="text" className='bg-dark w-100 text-light' placeholder="Enter your last name..." />
</FloatingLabel>
</Col>
</FormGroup>
<FloatingLabel
label="Username"
className="mb-3 w-100"
>
<Form.Control value={registerValues.username} onChange={e => setRegister({...registerValues, username: e.target.value})} type="text" className='bg-dark text-light responsive-input-register' placeholder="Enter your username..." />
</FloatingLabel>
<FloatingLabel
label="E-Mail"
className="mb-3"
>
<Form.Control value={registerValues.email} onChange={e => setRegister({...registerValues, email: e.target.value})} type="text" className='w-100 bg-dark text-light' placeholder="Enter your e-mail adress..." />
</FloatingLabel>
<FormGroup as={Row}>
<Col md='6' className='pe-1'>
<FloatingLabel
label="Password"
className="mb-3"
>
<Form.Control value={registerValues.password} onChange={e => setRegister({...registerValues, password: e.target.value})} type="password" className='bg-dark text-light' placeholder="Enter your password..." />
</FloatingLabel>
</Col>
<Col md='6' className='ps-1'>
<FloatingLabel
label="Confirm Password"
className="mb-3"
>
<Form.Control type="password" className='bg-dark text-light' placeholder="Confirm your password..." />
</FloatingLabel>
</Col>
</FormGroup>
{/* <FloatingLabel
label="Phone number"
className="mb-3"
>
<Form.Control type="text" className='w-100 bg-dark text-light' placeholder="Enter your phone number..." />
</FloatingLabel> */}
<input type='submit' className='btn py-2 mt-4 w-100 btn-main text-dark' value={'Sign in'} />
<Link className='text-light m-0 p-0' to='/'>
<p className='w-100 text-start pt-3'>Back to home</p>
</Link>
</Form>
)
}

export default RegisterForm

+ 45
- 0
Frontend/src/components/Requests/Requests.js 查看文件

@@ -0,0 +1,45 @@
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";

import styles from "./Requests.module.css";
import { fetchRoomsForWhichRequestExistAsync } from "../../store/request-slice";
import CustomAccordition from "../UI/CustomAccordition";
import { requestActions } from "../../store/request-slice";

export default function Requests() {
const { status, roomsForWhichRequestExist } = useSelector(
(state) => state.requests
);
const dispatch = useDispatch();

useEffect(() => {
dispatch(fetchRoomsForWhichRequestExistAsync());
}, [dispatch]);

const onEmptyRoom = (id) => {
dispatch(requestActions.onEmptyRoom(id));
};

return status === "pendingFetchRoomsForWhichRequestExist" ? (
<p>Loading...</p>
) : (
<div className={styles.container}>
<h3 className="p-0 m-0 py-3 text-center w-50 fw-light text-muted">
Requests
</h3>
<div className={styles.requestContainer}>
{roomsForWhichRequestExist &&
roomsForWhichRequestExist.length > 0 &&
roomsForWhichRequestExist.map((room, index) => (
<CustomAccordition
room={room}
key={index}
onEmptyRoom={onEmptyRoom}
/>
))}
{roomsForWhichRequestExist &&
roomsForWhichRequestExist.length === 0 && <p>No requests</p>}
</div>
</div>
);
}

+ 4
- 0
Frontend/src/components/Requests/Requests.module.css 查看文件

@@ -0,0 +1,4 @@
.requestContainer{
display: flex;
flex-direction: column;
}

+ 74
- 0
Frontend/src/components/SideNavbar.js 查看文件

@@ -0,0 +1,74 @@
import React, { useContext } from "react";
import { FiBell, FiUser, FiMessageSquare, FiLogOut } from "react-icons/fi";
import { UserContext } from "../contexts/userContext";
import { useDispatch } from "react-redux";
import { chatActions } from "../store/chat-slice";
import {FaArrowAltCircleRight} from 'react-icons/fa';
import { Link } from "react-router-dom";

const SideNavbar = () => {
const { user, logOut } = useContext(UserContext);
const dispatch = useDispatch();

const logOutHandler = () => {
if (window.confirm("Are you sure?")) {
// ovde ce ici logika za logovanje
// uklanjanje iz state-a i slicno
dispatch(chatActions.deleteActiveRoom());
logOut();
}
};

return (
<div className="min-vh-100 bg-white border-right px-1 d-flex flex-column pt-5">
<Link
to={'/main'}
className="btn btn-white w-100 button-block button-block-flex-column"
// onClick={() => onClickHandler("notifications")}
>
<FiBell className="icon-fs" />
<h5 className="small text-muted text-center fw-light">feed</h5>
</Link>

<Link
to={'/chats'}
className="btn btn-white button-block mt-4 w-100 button-block-flex-column"
// onClick={() => onClickHandler("chats")}
>
<FiMessageSquare className="icon-fs" />
<h5 className="small text-muted text-center fw-light">chat</h5>
</Link>
{
(user && user.roles.includes('Support') === true) &&
<Link
to={'/requests'}
className="btn btn-white button-block mt-4 w-100 button-block-flex-column"
// onClick={() => onClickHandler("requests")}
>
<FaArrowAltCircleRight className="icon-fs" />
<h5 className="small text-muted text-center fw-light">Requests</h5>
</Link>
}
{user && (
<Link
to={'/profile'}
className="btn btn-white button-block mt-4 w-100 button-block-flex-column"
>
<FiUser className="icon-fs" />
<h5 className="small text-muted text-center fw-light">profile</h5>
</Link>
)}
<button
className="btn btn-white button-block mt-4 w-100 button-block-flex-column"
onClick={logOutHandler}
>
<FiLogOut className="icon-fs" />
<h5 className="small p-0 m-0 text-muted text-center fw-light">
log out
</h5>
</button>
</div>
);
};

export default SideNavbar;

+ 66
- 0
Frontend/src/components/UI/CustomAccordition.js 查看文件

@@ -0,0 +1,66 @@
import React, { useState } from "react";
// import { useSelector, useDispatch } from "react-redux";

import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import Typography from "@mui/material/Typography";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import CustomerRequest from "../CustomerRequest/CustomerRequest";

import requestService from "../../services/requestService";

const CustomAccordition = ({ room, onEmptyRoom }) => {
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
const [expanded, setExpanded] = useState(false);

const showCustomerRequests = () => {
if (expanded === false) {
requestService
.getCusotmersForSpecificRequestRoom(room.id)
.then((res) => setCustomers(res))
.catch((e) => console.log(e))
.finally(() => setLoading(false));
}

setExpanded((oldState) => !oldState);
};

const requestHandled = (id) => {
let tmpArr = [...customers];
tmpArr = tmpArr.filter((customer) => customer.id !== id);
setCustomers(tmpArr);
if (tmpArr.length === 0) {
onEmptyRoom(room.id);
}
};

return (
<Accordion style={{ marginTop: 10 }} expanded={expanded}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
onClick={() => showCustomerRequests()}
>
<Typography>{room.name}</Typography>
</AccordionSummary>
{loading ? (
<p>Loading...</p>
) : (
<AccordionDetails>
{customers.map((customer, index) => (
<CustomerRequest
key={index}
customer={{ roomId: room.id, ...customer }}
requestHandled={requestHandled}
/>
))}
</AccordionDetails>
)}
</Accordion>
);
};

export default CustomAccordition;

+ 53
- 0
Frontend/src/components/UI/Dialog.js 查看文件

@@ -0,0 +1,53 @@
import React from "react";
import Backdrop from "@mui/material/Backdrop";
import Box from "@mui/material/Box";
import Modal from "@mui/material/Modal";
import Fade from "@mui/material/Fade";
import Typography from "@mui/material/Typography";

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
border: "none",
boxShadow: 24,
};

const Dialog = ({ changeModalVisibility, open, acceptHandler }) => {
const handleClose = () => changeModalVisibility();

return (
<div>
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500,
}}
>
<Fade in={open}>
<Box className="bg-dark rounded p-5" sx={style}>
<Typography className="text-light" id="transition-modal-title" variant="h5" component="h2">
Are you sure you want to send a request?
</Typography>
<Typography className="text-light" id="transition-modal-description" sx={{ mt: 2 }}>
Duis mollis, est non commodo luctus, nisi erat porttitor ligula.
</Typography>
<div style={{marginTop: '2rem', display: 'flex', justifyContent: 'right'}}>
<button className="btn-main text-light w-50 btn" style={{marginLeft: '0.5rem'}} onClick={handleClose}>Close</button>
<button className="btn-main text-light btn w-50" style={{marginLeft: '0.5rem'}} onClick={acceptHandler}>Send Request</button>
</div>
</Box>
</Fade>
</Modal>
</div>
);
};

export default Dialog;

+ 5
- 0
Frontend/src/config/urls.js 查看文件

@@ -0,0 +1,5 @@
export const apiUrl = 'http://localhost:5116/v1.0'

export const accountsUrl = `${apiUrl}/Customer`

export const chatsUrl = `${apiUrl}/Chat`

+ 31
- 0
Frontend/src/contexts/userContext.js 查看文件

@@ -0,0 +1,31 @@
import { createContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

export const UserContext = createContext();

export const UserProvider = (props) => {
const [user, setUser] = useState(null);
const navigate = useNavigate();

// if (JSON.parse(localStorage.getItem('user'))) {
// axios.defaults.headers.common['Authorization'] = `Bearer ${JSON.parse(localStorage.getItem('user')).token}`;
// }

useEffect(() => {
if (localStorage.getItem("user") !== undefined) {
setUser(JSON.parse(localStorage.getItem("user")));
}
}, []);

const logOut = () => {
localStorage.removeItem("user");
setUser(null);
navigate("/login");
};

return (
<UserContext.Provider value={{ user, setUser, logOut }}>
{props.children}
</UserContext.Provider>
);
};

+ 41
- 0
Frontend/src/dummyData.js 查看文件

@@ -0,0 +1,41 @@
export const dummyChats = [
{
chatId: 1,
chatName: 'Chat 1',
},
{
chatId: 2,
chatName: 'Chat 2',
},
{
chatId: 3,
chatName: 'Chat 3',
},
{
chatId: 4,
chatName: 'Chat 4',
},
]

export const dummyFeed = [
{
feedId: 1,
feedName: 'Feed Item 1',
since: '5 minutes ago'
},
{
feedId: 2,
feedName: 'Feed Item 2',
since: 'Yesterday'
},
{
feedId: 3,
feedName: 'Feed Item 3',
since: '27/06/2022',
},
{
feedId: 4,
feedName: 'Feed Item 4',
since: '27/06/2022',
},
]

+ 103
- 0
Frontend/src/index.css 查看文件

@@ -0,0 +1,103 @@
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* ======================================= root vars */
:root {
--main-bg: linear-gradient(45deg,#00c9ff, #92fe9d);
--main: #00c9ff;
--secondary: #92fe9d;

--medium-font-size: 1.5rem;
--icon-font-size: 2.25rem;
}
/* ======================================= backgrounds */
.bg-main{
background: var(--main-bg) !important;
}
.bg-light-transparent{
background: rgba(255,255,255,0.75);
}
/* ======================================= heights & widths */
.h-100-auto-overflow{
height: 100vh !important;
overflow-y: auto !important;
}
/* ======================================= borders */
.border-right{
border-right: 2px solid #ababab;
}
/* ======================================= typography */
.medium-fs{
font-size: var(--medium-font-size);
}
.icon-fs{
font-size: var(--icon-font-size);
}
/* ======================================= buttons */
.btn-main{
background-color: var(--main) !important;
}
.button-block{
height: 50px;
width: 50px;
}
.button-block-flex-column{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* ======================================= flex */
.flex-center{
display: flex;
justify-content: center !important;
align-items: center !important;
}
/* ======================================= links */
.home-link{
height: 200px !important;
width: 350px !important;
transition: 0.35s;
}
.home-link:hover{
transform: scale(1.025);
transition: 0.35s;
}
/* ======================================= forms */
.responsive-input{
width: 325px !important;
}
.responsive-input-register{
width: 575px !important;
}
@media screen and (max-width: 800px){
.responsive-input-register{
width: 100% !important;
}
}
input[type=submit]{
font-size: 1.25rem !important;
text-transform: uppercase;
}

.messages{
height: calc(100% - 160px);
overflow-y: scroll;
}
/* ======================================= overrides */
.overflow-auto{
overflow-y: auto !important;
}
/* ======================================= chat */
.message{
max-width: 70%;
}

+ 25
- 0
Frontend/src/index.js 查看文件

@@ -0,0 +1,25 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import "bootstrap/dist/css/bootstrap.min.css";
import { Provider } from "react-redux";
import store from "./store/index";
import { BrowserRouter } from "react-router-dom";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
// <React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<App />
</Provider>
</BrowserRouter>
// </React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

+ 1
- 0
Frontend/src/logo.svg 查看文件

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

+ 13
- 0
Frontend/src/reportWebVitals.js 查看文件

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};

export default reportWebVitals;

+ 17
- 0
Frontend/src/screens/ChatsScreen.js 查看文件

@@ -0,0 +1,17 @@
import React from 'react'
import { Container } from 'react-bootstrap'
import MainContainer from '../components/MainContainer'
import MiddleContainer from '../components/MiddleContainer'
import SideNavbar from '../components/SideNavbar'

const ChatsScreen = () => {
return (
<Container fluid className='m-0 p-0 min-vh-100 bg-main d-flex justify-content-start'>
<SideNavbar/>
<MiddleContainer showTerm={'chats'}/>
<MainContainer showTerm={'chats'}/>
</Container>
)
}

export default ChatsScreen

+ 38
- 0
Frontend/src/screens/HomeScreen.js 查看文件

@@ -0,0 +1,38 @@
import React,{useContext, useEffect} from "react";
import { Container } from "react-bootstrap";
import { Link, useNavigate } from "react-router-dom";
import { UserContext } from "../contexts/userContext";

const HomeScreen = () => {
const navigate = useNavigate();
const { user } = useContext(UserContext);

useEffect(() => {
if (user) {
navigate("main");
}
}, [user, navigate]);

return (
<Container fluid className="p-0 m-0 min-vh-100 bg-main flex-center">
<div className="d-flex flex-md-row">
<div className="me-1">
<Link className="text-light text-decoration-none" to={"/login"}>
<div className="home-link rounded bg-dark flex-center">
<p className="display-6">Sign In</p>
</div>
</Link>
</div>
<div className="ms-1">
<Link className="text-light text-decoration-none" to={"/register"}>
<div className="home-link rounded bg-dark flex-center">
<p className="display-6">Sign Up</p>
</div>
</Link>
</div>
</div>
</Container>
);
};

export default HomeScreen;

+ 13
- 0
Frontend/src/screens/LoginScreen.js 查看文件

@@ -0,0 +1,13 @@
import React from 'react'
import { Container } from 'react-bootstrap'
import LogInForm from '../components/LogInForm'

const LoginScreen = () => {
return (
<Container fluid className='m-0 p-0 min-vh-100 bg-main flex-center'>
<LogInForm />
</Container>
)
}

export default LoginScreen

+ 18
- 0
Frontend/src/screens/MainScreen.js 查看文件

@@ -0,0 +1,18 @@
import React from 'react'
import { Container } from 'react-bootstrap'
import MainContainer from '../components/MainContainer'
import MiddleContainer from '../components/MiddleContainer'
import SideNavbar from '../components/SideNavbar'

const MainScreen = () => {

return (
<Container fluid className='m-0 p-0 min-vh-100 bg-main d-flex justify-content-start'>
<SideNavbar/>
<MiddleContainer showTerm={'notifications'}/>
<MainContainer showTerm={'notifications'}/>
</Container>
)
}

export default MainScreen

+ 13
- 0
Frontend/src/screens/RegisterScreen.js 查看文件

@@ -0,0 +1,13 @@
import React from 'react'
import { Container } from 'react-bootstrap'
import RegisterForm from '../components/RegisterForm'

const RegisterScreen = () => {
return (
<Container fluid className='m-0 p-0 min-vh-100 bg-main flex-center'>
<RegisterForm />
</Container>
)
}

export default RegisterScreen

+ 17
- 0
Frontend/src/screens/RequestScreen.js 查看文件

@@ -0,0 +1,17 @@
import React from 'react'
import { Container } from 'react-bootstrap'
import MainContainer from '../components/MainContainer'
import MiddleContainer from '../components/MiddleContainer'
import SideNavbar from '../components/SideNavbar'

const RequestScreen = () => {
return (
<Container fluid className='m-0 p-0 min-vh-100 bg-main d-flex justify-content-start'>
<SideNavbar/>
<MiddleContainer showTerm={'requests'}/>
<MainContainer showTerm={'empty'}/>
</Container>
)
}

export default RequestScreen

+ 13
- 0
Frontend/src/services/chatService.js 查看文件

@@ -0,0 +1,13 @@
import axios from "axios";
import { apiUrl } from "../config/urls";

axios.defaults.baseURL = apiUrl;

const responseBody = (response) => response.data;

const methods = {
getChats: () => axios.get("/Chat").then(responseBody),
createChat: (payload) => axios.post("/Chat", payload).then(responseBody),
};

export default methods;

+ 22
- 0
Frontend/src/services/requestService.js 查看文件

@@ -0,0 +1,22 @@
import axios from "axios";
import { apiUrl } from "../config/urls";

axios.defaults.baseURL = apiUrl;

const responseBody = (response) => response.data;

const methods = {
getRequests: () => axios.get("/request").then(responseBody),
sendJoinRequest: (payload) =>
axios.post("/request/join-request", payload).then(responseBody),
getRoomsForWhichRequestExist: () =>
axios.get("/request/request-rooms").then(responseBody),
getCusotmersForSpecificRequestRoom: (payload) =>
axios.get(`/request/room-customers?id=${payload}`).then(responseBody),
sendAcceptCustomerRequest: (payload) =>
axios.post("/request/accept-request", payload).then(responseBody),
rejectCustomerRequest:(payload) =>
axios.post("/request/reject-request",payload).then(responseBody)
};

export default methods;

+ 15
- 0
Frontend/src/services/userService.js 查看文件

@@ -0,0 +1,15 @@
import axios from "axios";
import { apiUrl } from "../config/urls";

axios.defaults.baseURL = apiUrl

const responseBody = response => response.data

// const queryToken = localStorage.getItem("user") ? `?token=${JSON.parse(localStorage.getItem("user").token)}` : ''

const methods = {
logIn: (loginValues) => axios.post(`/Customer/login`, loginValues).then(responseBody),
register: (registerValues) => axios.post('/Customer/register', registerValues).then(responseBody)
}

export default methods

+ 5
- 0
Frontend/src/setupTests.js 查看文件

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

+ 127
- 0
Frontend/src/store/chat-slice.js 查看文件

@@ -0,0 +1,127 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import chatService from "../services/chatService";

const initialState = {
status: "idle",
rooms: [],
activeRoom: null,
error: null,
// Hub connection
connection: null,
// List of room messages
messages: [],
// All active user connections to rooms
connections: [],
};

export const fetchChatRoomsAsync = createAsyncThunk(
"chat/fetchChatRoomsAsync",
async (_, thunkAPI) => {
try {
return await chatService.getChats();
} catch (error) {
return thunkAPI.rejectWithValue({ error });
}
}
);

export const createChatRoomAsync = createAsyncThunk(
"chat/createChatRoomAsync",
async (payload, thunkAPI) => {
try {
return await chatService.createChat(payload);
} catch (error) {
return thunkAPI.rejectWithValue({ error });
}
}
);

const chatSlice = createSlice({
name: "chat",
initialState,
reducers: {
setAllChats: (state, action) => {
state.rooms = action.payload;
},
setRoom: (state, action) => {
state.activeRoom = action.payload;
},
deleteActiveRoom: (state, action) => {
state.activeRoom = null;
},
// Set hub connection
setConnection: (state, action) => {
state.connection = action.payload;
},
// New message sent from user
newMessage: (state, action) => {
if (action.payload.changedRoom) {
state.messages = [];
} else {
state.messages = [...state.messages, action.payload];
}
},
saveContextId: (state, action) => {
// Check is empty array
// If array is not empty, check is connection for specific room and specific user already added
if (state.connections.length > 0) {
if (
!state.connections.some(
(e) => e.connId === action.payload && e.roomId === state.activeRoom
)
) {
state.connections.push({
connId: action.payload.connId,
roomId: state.activeRoom.id,
userId: action.payload.userId,
});
}
} else {
// If array is empty, add connection
state.connections.push({
connId: action.payload.connId,
roomId: state.activeRoom.id,
userId: action.payload.userId,
});
}
},
// Set messages fetched from backend for specific room
setMessages: (state, action) => {
state.messages = action.payload;
},
},
extraReducers: (builder) => {
// Fetch chat rooms
builder.addCase(fetchChatRoomsAsync.pending, (state) => {
state.status = "pendingFetchRooms";
state.error = null;
});
builder.addCase(fetchChatRoomsAsync.fulfilled, (state, action) => {
state.rooms = action.payload;
state.status = "idle";
state.error = null;
});
builder.addCase(fetchChatRoomsAsync.rejected, (state, action) => {
state.status = "idle";
state.error = "Fetch error" + action.payload;
});

// Add chat room
builder.addCase(createChatRoomAsync.pending, (state) => {
state.status = "pendingAddRoom";
state.error = null;
});
builder.addCase(createChatRoomAsync.fulfilled, (state, action) => {
state.status = "idle";
state.rooms = [...state.rooms, action.payload];
state.error = null;
});
builder.addCase(createChatRoomAsync.rejected, (state, action) => {
state.status = "idle";
state.error = action.payload;
});
},
});

export const chatActions = chatSlice.actions;
export const chatReducers = chatSlice.reducer;

+ 14
- 0
Frontend/src/store/index.js 查看文件

@@ -0,0 +1,14 @@
import { configureStore } from "@reduxjs/toolkit";
import { uiReducers } from "./ui-slice";
import { chatReducers } from "./chat-slice";
import { requestsReducers } from "./request-slice";

const store = configureStore({
reducer: { ui: uiReducers, chat: chatReducers, requests: requestsReducers },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
});

export default store;

+ 113
- 0
Frontend/src/store/request-slice.js 查看文件

@@ -0,0 +1,113 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import requestService from "../services/requestService";

const initialState = {
requests: null,
status: "idle",
error: null,
chosenRoom: null,
roomsForWhichRequestExist: null,
};

export const fetchRequestsAsync = createAsyncThunk(
"requests/fetchChatRoomsAsync",
async (_, thunkAPI) => {
try {
return await requestService.getRequests();
} catch (error) {
return thunkAPI.rejectWithValue({ error });
}
}
);

export const createJoinRequestAsync = createAsyncThunk(
"requests/createJoinRequestAsync",
async (payload, thunkAPI) => {
try {
return await requestService.sendJoinRequest(payload);
} catch (error) {
return thunkAPI.rejectWithValue({ error });
}
}
);

export const fetchRoomsForWhichRequestExistAsync = createAsyncThunk(
"requests/fetchRoomsForWhichRequestExistAsync",
async (_, thunkAPI) => {
try {
return await requestService.getRoomsForWhichRequestExist();
} catch (error) {
return thunkAPI.rejectWithValue({ error });
}
}
);

const requestslice = createSlice({
name: "requests",
initialState,
reducers: {
chooseRoom: (state, action) => {
state.chosenRoom = action.payload;
},
onEmptyRoom: (state, action) => {
state.roomsForWhichRequestExist = state.roomsForWhichRequestExist.filter(
(room) => room.id !== action.payload
);
},
},
extraReducers: (builder) => {
// Fetch All Requests
builder.addCase(fetchRequestsAsync.pending, (state) => {
state.status = "pendingFetchRequests";
state.error = null;
});
builder.addCase(fetchRequestsAsync.fulfilled, (state, action) => {
state.status = "idle";
state.requests = action.payload;
state.error = null;
});
builder.addCase(fetchRequestsAsync.rejected, (state, action) => {
state.status = "idle";
state.error = action.payload;
});

// Create join request
builder.addCase(createJoinRequestAsync.pending, (state) => {
state.status = "pendingAddRoom";
state.error = null;
});
builder.addCase(createJoinRequestAsync.fulfilled, (state, action) => {
state.status = "idle";
state.requests = [...state.requests, action.payload];
state.error = null;
});
builder.addCase(createJoinRequestAsync.rejected, (state, action) => {
state.status = "idle";
state.error = action.payload;
});

// Get rooms for which request exist
builder.addCase(fetchRoomsForWhichRequestExistAsync.pending, (state) => {
state.status = "pendingFetchRoomsForWhichRequestExist";
state.error = null;
});
builder.addCase(
fetchRoomsForWhichRequestExistAsync.fulfilled,
(state, action) => {
state.status = "idle";
state.roomsForWhichRequestExist = action.payload;
state.error = null;
}
);
builder.addCase(
fetchRoomsForWhichRequestExistAsync.rejected,
(state, action) => {
state.status = "idle";
state.error = action.payload;
}
);
},
});

export const requestActions = requestslice.actions;
export const requestsReducers = requestslice.reducer;

+ 16
- 0
Frontend/src/store/ui-slice.js 查看文件

@@ -0,0 +1,16 @@
import { createSlice } from "@reduxjs/toolkit";

const initialState = { showModal: false };

const uiSlice = createSlice({
name: "ui",
initialState,
reducers: {
changeShowModal: (state, action) => {
state.showModal = !state.showModal;
},
},
});

export const uiActions = uiSlice.actions;
export const uiReducers = uiSlice.reducer;

正在加载...
取消
保存