Compare commits

...

58 Commits
v0.1.0 ... main

Author SHA1 Message Date
Nikolai Petukhov
3822ad0dce fixed exit button 2024-10-22 19:25:49 +03:00
Nikolai Petukhov
2829c11e4c 0.5.4 2024-10-16 23:48:44 +03:00
Nikolai Petukhov
0bcdb95b57 change to dev api 2024-10-16 23:48:26 +03:00
Nikolai Petukhov
cabe02be57 0.5.3 2024-10-16 23:43:37 +03:00
Nikolai Petukhov
59d4a44079 integrated redux library 2024-10-16 23:40:34 +03:00
Nikolai Petukhov
fde1f8ecfe fix chats sorting 2024-10-16 23:16:06 +03:00
Nikolai Petukhov
9b4870995f 0.5.2 2024-10-12 12:44:15 +03:00
Nikolai Petukhov
2f447cef1a fixed init routes 2024-10-12 12:44:04 +03:00
Nikolai Petukhov
964ca236e8 0.5.1 2024-10-12 12:33:46 +03:00
Nikolai Petukhov
9c1c670ccb small fix with routes 2024-10-12 12:25:38 +03:00
Nikolai Petukhov
ff9bd3ac8c 0.5.0 2024-10-12 12:24:16 +03:00
Nikolai Petukhov
51618c8858 small changes 2024-10-12 12:23:25 +03:00
Nikolai Petukhov
54f6c5c053 config fix 2024-10-12 11:39:17 +03:00
Nikolai Petukhov
8fecf7cb1f I have added intervals for messages 2024-10-11 13:39:15 +03:00
Nikolai Petukhov
49a8af611f done 2024-10-10 12:03:37 +03:00
Nikolai Petukhov
7c4457dea4 done 2024-10-10 12:02:10 +03:00
Nikolai Petukhov
6096bdc4cb done 2024-10-10 12:01:49 +03:00
Askar Akhmetkhanov
dd10b080e8 0.4.0 2024-10-09 17:38:38 +03:00
Askar Akhmetkhanov
4cf909c607 Chat, scrolling, sockets and timestamps updated 2024-10-09 17:37:50 +03:00
Nikolai Petukhov
13f4d43761 chats and messages 2024-10-05 11:46:18 +03:00
Nikolai Petukhov
22a549e269 an 2024-10-04 16:06:48 +03:00
Nikolai Petukhov
25c3e16c74 changing fields in account is done and upd chats retrieval 2024-10-04 15:53:50 +03:00
Nikolai Petukhov
86db5df813 chat retrieval is done 2024-10-04 14:29:00 +03:00
Nikolai Petukhov
d1e824ab77 retrieving chats 2024-10-04 11:21:21 +03:00
Nikolai Petukhov
073c61977f init for chats and api 2024-10-04 00:06:44 +03:00
Nikolai Petukhov
1301c145e8 check json format 2024-10-03 23:20:40 +03:00
Nikolai Petukhov
ac6dffa129 check json format 2024-10-03 23:12:38 +03:00
Nikolai Petukhov
8e4cad4c85 check json format 2024-10-03 23:06:18 +03:00
Nikolai Petukhov
d1091e570b check json format 2024-10-03 23:01:38 +03:00
Nikolai Petukhov
4a5041a65e check json format 2024-10-03 22:48:57 +03:00
Nikolai Petukhov
8d0fadc906 change name action for api 2024-10-03 22:35:39 +03:00
Nikolai Petukhov
6bea0428f4 api link fix 2024-10-03 21:26:02 +03:00
Nikolai Petukhov
660f2e9d5c 0.3.0 2024-10-03 21:16:23 +03:00
Nikolai Petukhov
a9b683797b auth with api 2024-10-03 21:15:48 +03:00
Nikolai Petukhov
a3484f4525 backend init 2024-09-28 12:51:59 +03:00
Nikolai Petukhov
876ef28221 0.2.4 2024-09-28 10:37:42 +03:00
Nikolai Petukhov
e6231f86b4 account page is done 2024-09-28 10:34:06 +03:00
Nikolai Petukhov
9d13f571d9 0.2.3 2024-09-28 10:00:38 +03:00
Nikolai Petukhov
f654851e1a fix dependencies 2024-09-28 09:56:43 +03:00
Askar Akhmetkhanov
3fb107fd8b Commit 2024-09-28 00:12:00 +03:00
Nikolai Petukhov
2a881f3920 small fix 2024-09-21 17:25:03 +03:00
Nikolai Petukhov
762c4867b7 0.2.2 2024-09-21 17:05:58 +03:00
Nikolai Petukhov
2a56274ca9 login pages are done 2024-09-21 17:03:09 +03:00
Nikolai Petukhov
93ed4ce7fc home page is done 2024-09-21 15:53:37 +03:00
Nikolai Petukhov
2ce5bb8d7a upd init page structure 2024-09-21 13:53:46 +03:00
Nikolai Petukhov
e75288a6f0 init page is done 2024-09-21 13:32:52 +03:00
Nikolai Petukhov
963c94c262 last preparation 2024-09-21 12:42:07 +03:00
Nikolai Petukhov
d7de0c4353 fix 2024-09-20 16:10:14 +03:00
Nikolai Petukhov
a482be764b ijl to brojs 2024-09-20 15:05:14 +03:00
Nikolai Petukhov
6fc51fd45c try to fix 2024-09-20 12:18:35 +03:00
Nikolai Petukhov
59f5335e52 upd import of css 2024-09-20 12:13:47 +03:00
Nikolai Petukhov
8c5af38e3c upd imports in css 2024-09-20 11:58:13 +03:00
Nikolai Petukhov
811e9448b0 0.2.1 2024-09-20 11:01:45 +03:00
Nikolai Petukhov
5ef5ca35b2 init structure is completed 2024-09-20 11:01:23 +03:00
Nikolai Petukhov
626d556e1a 0.2.0 2024-09-14 13:29:56 +03:00
Nikolai Petukhov
d86fa96a3e upd routes again 2024-09-14 13:11:32 +03:00
Nikolai Petukhov
fed00edfd4 test routes 2024-09-14 12:58:07 +03:00
Nikolai Petukhov
aa40f3791b upd routes 2024-09-14 12:48:29 +03:00
74 changed files with 5596 additions and 126 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
TOKEN_KEY=5frv12e4few3r

3
.gitignore vendored
View File

@ -1 +1,2 @@
node_modules
node_modules
.idea

5
.idea/.gitignore generated vendored
View File

@ -1,5 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

12
.idea/front.iml generated
View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/front.iml" filepath="$PROJECT_DIR$/.idea/front.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

28
bro.config.js Normal file
View File

@ -0,0 +1,28 @@
const pkg = require("./package");
module.exports = {
apiPath: "stubs/api",
webpackConfig: {
output: {
publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/`,
},
},
/* use https://admin.bro-js.ru/ to create config, navigations and features */
navigations: {
'enterfront.main': '/enterfront',
'enterfront.home': '/enterfront/home',
'enterfront.auth': '/enterfront/auth',
'enterfront.reg': '/enterfront/reg',
'enterfront.account': '/enterfront/account',
'enterfront.chat': '/enterfront/chat',
},
features: {
"enterfront": {
// add your features here in the format [featureName]: { value: string }
},
},
config: {
"enterfront.api": "/api",
// paste stand URL to config
},
};

View File

@ -1,23 +0,0 @@
const pkg = require('./package')
module.exports = {
apiPath: 'stubs/api',
webpackConfig: {
output: {
publicPath: `/static/${pkg.name}/${process.env.VERSION || pkg.version}/`
}
},
navigations: {
'enterfront.main': '/enterfront',
'link.enterfront.auth': '/auth',
'link.enterfront.account': '/account',
},
features: {
'undefined': {
// add your features here in the format [featureName]: { value: string }
},
},
config: {
'undefined.api': '/api',
}
}

18
images/chat.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 58 58" xml:space="preserve">
<g>
<path style="fill:#FFA500FF;" d="M25,9.586C11.193,9.586,0,19.621,0,32c0,4.562,1.524,8.803,4.135,12.343
C3.792,48.433,2.805,54.194,0,57c0,0,8.47-1.191,14.273-4.651c0.006-0.004,0.009-0.01,0.014-0.013
c1.794-1.106,3.809-2.397,4.302-2.783c0.301-0.417,0.879-0.543,1.328-0.271c0.298,0.181,0.487,0.512,0.488,0.86
c0.003,0.582-0.008,0.744-3.651,3.018c2.582,0.81,5.355,1.254,8.245,1.254c13.807,0,25-10.035,25-22.414S38.807,9.586,25,9.586z"/>
<path style="fill:#EF9400FF;" d="M58,23.414C58,11.035,46.807,1,33,1c-9.97,0-18.575,5.234-22.589,12.804
C14.518,11.153,19.553,9.586,25,9.586c13.807,0,25,10.035,25,22.414c0,4.735-1.642,9.124-4.437,12.743
C51.162,47.448,58,48.414,58,48.414c-2.805-2.805-3.792-8.566-4.135-12.657C56.476,32.217,58,27.976,58,23.414z"/>
<path style="fill:#FFFFFF;" d="M32.5,26h-14c-0.552,0-1-0.447-1-1s0.448-1,1-1h14c0.552,0,1,0.447,1,1S33.052,26,32.5,26z"/>
<path style="fill:#FFFFFF;" d="M38,33H13c-0.552,0-1-0.447-1-1s0.448-1,1-1h25c0.552,0,1,0.447,1,1S38.552,33,38,33z"/>
<path style="fill:#FFFFFF;" d="M38,40H13c-0.552,0-1-0.447-1-1s0.448-1,1-1h25c0.552,0,1,0.447,1,1S38.552,40,38,40z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

18
images/chat2.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 58 58" xml:space="preserve">
<g>
<path style="fill:#0391FD;" d="M25,9.586C11.193,9.586,0,19.621,0,32c0,4.562,1.524,8.803,4.135,12.343
C3.792,48.433,2.805,54.194,0,57c0,0,8.47-1.191,14.273-4.651c0.006-0.004,0.009-0.01,0.014-0.013
c1.794-1.106,3.809-2.397,4.302-2.783c0.301-0.417,0.879-0.543,1.328-0.271c0.298,0.181,0.487,0.512,0.488,0.86
c0.003,0.582-0.008,0.744-3.651,3.018c2.582,0.81,5.355,1.254,8.245,1.254c13.807,0,25-10.035,25-22.414S38.807,9.586,25,9.586z"/>
<path style="fill:#0F71D3;" d="M58,23.414C58,11.035,46.807,1,33,1c-9.97,0-18.575,5.234-22.589,12.804
C14.518,11.153,19.553,9.586,25,9.586c13.807,0,25,10.035,25,22.414c0,4.735-1.642,9.124-4.437,12.743
C51.162,47.448,58,48.414,58,48.414c-2.805-2.805-3.792-8.566-4.135-12.657C56.476,32.217,58,27.976,58,23.414z"/>
<path style="fill:#FFFFFF;" d="M32.5,26h-14c-0.552,0-1-0.447-1-1s0.448-1,1-1h14c0.552,0,1,0.447,1,1S33.052,26,32.5,26z"/>
<path style="fill:#FFFFFF;" d="M38,33H13c-0.552,0-1-0.447-1-1s0.448-1,1-1h25c0.552,0,1,0.447,1,1S38.552,33,38,33z"/>
<path style="fill:#FFFFFF;" d="M38,40H13c-0.552,0-1-0.447-1-1s0.448-1,1-1h25c0.552,0,1,0.447,1,1S38.552,40,38,40z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/home_wp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

9
images/user.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 49 KiB

2606
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,35 @@
{
"dependencies": {
"@brojs/cli": "^1.0.0",
"@brojs/create": "^1.0.0",
"@ijl/cli": "^5.1.0",
"@reduxjs/toolkit": "^2.3.0",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"dotenv": "^16.4.5",
"emoji-mart": "^5.6.0",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-emoji-picker": "^1.0.13",
"react-icons": "^5.3.0",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.1",
"typescript": "^5.5.4"
"react-toastify": "^10.0.5",
"socket.io": "^4.8.0",
"socket.io-client": "^4.8.0",
"styled-components": "^6.1.13",
"typescript": "^5.5.4",
"ws": "^8.18.0"
},
"main": "./src/index.tsx",
"scripts": {
"start": "ijl-cli server --port=8099 --with-open-browser",
"build": "npm run clean && ijl-cli build --dev",
"build:prod": "npm run clean && ijl-cli build",
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build",
"clean": "rimraf dist"
},
"name": "enterfront",
"version": "0.1.0"
"version": "0.5.4"
}

View File

@ -1,16 +1,24 @@
import { getNavigations, getNavigationsValue } from '@ijl/cli';
import { getNavigations, getNavigationsValue } from '@brojs/cli';
import pkg from '../../package.json';
const baseUrl = getNavigationsValue(`${(pkg as any).name}.main`);
const navs = getNavigations();
const makeUrl = url => baseUrl + url;
export const URLs = {
baseUrl,
baseUrl, // init
home: {
url: navs[`${(pkg as any).name}.home`],
},
chat: {
url: navs[`${(pkg as any).name}.chat`],
},
auth: {
url: makeUrl(navs[`link.${(pkg as any).name}.auth`]),
url: navs[`${(pkg as any).name}.auth`], // sign in
},
reg: {
url: navs[`${(pkg as any).name}.reg`], // sign up
},
account: {
url: makeUrl(navs[`link.${(pkg as any).name}.account`]),
}
url: navs[`${(pkg as any).name}.account`],
},
};

View File

@ -1,15 +1,43 @@
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import React, {useEffect} from 'react';
import { BrowserRouter } from 'react-router-dom';
import { Dashboard } from './dashboard';
import {ToastContainer} from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';
import { Provider } from 'react-redux';
import store from './backend/redux/store.js'; // Import your store
import './index.css'
import {displayMessage} from "./backend/notifications/notifications.js";
import {MessageType} from "./backend/notifications/message";
const App = () => {
useEffect(() => {
document.title = 'Enterfront';
}, []);
useEffect(() => {
const msg = localStorage.getItem('message');
if (!msg) return;
displayMessage(msg, MessageType.SUCCESS);
localStorage.removeItem('message');
}, []);
return(
<BrowserRouter>
<Dashboard />
</BrowserRouter>
<Provider store={store}>
<BrowserRouter>
<Dashboard />
</BrowserRouter>
<ToastContainer/>
</Provider>
)
}
export default App;

60
src/backend/api.js Normal file
View File

@ -0,0 +1,60 @@
import {getConfigValue} from "@brojs/cli";
const LOCAL = "http://localhost:8099";
const DEV = "https://dev.bro-js.ru";
export const BASE_API_URL = DEV + getConfigValue("enterfront.api");
// fetch(`${BASE_API_URL}/books/list`)
export async function post(path, body) {
const token = localStorage.getItem('token');
const res = await fetch(`${BASE_API_URL}${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": token ? `Bearer ${token}` : undefined
},
body: JSON.stringify(body)
});
console.log("Initial data from API:", res)
const data = JSON.parse(await res.text());
console.log("Data from API:", data)
if (res.status === 200) {
console.log("Received post:", data);
return {ok: true, data: data};
} else {
console.log("Error during post:", data.message);
return {ok: false, data: data};
}
}
export async function get(path){
const token = localStorage.getItem('token');
const res = await fetch(`${BASE_API_URL}${path}`, {
method: "GET",
headers: {
"Authorization": token ? `Bearer ${token}` : undefined
}
});
console.log("Data from API:", res)
const data = await res.json();
if (res.status === 200) {
console.log("Received get:", data);
return {ok: true, data: data};
} else {
console.log("Error during get:", data.message);
return {ok: false, data: data};
}
}

View File

@ -0,0 +1,6 @@
export enum MessageType {
ERROR,
SUCCESS,
INFO,
WARN
}

View File

@ -0,0 +1,28 @@
import {toast} from "react-toastify";
import {MessageType} from "./message.tsx";
export const displayMessage = (message, type) => {
switch (type) {
default:
case MessageType.ERROR:
toast.error(message, {
position: 'bottom-right',
});
break;
case MessageType.INFO:
toast.info(message, {
position: 'bottom-right',
});
break;
case MessageType.SUCCESS:
toast.success(message, {
position: 'bottom-right',
});
break;
case MessageType.WARN:
toast.warn(message, {
position: 'bottom-right',
});
break;
}
}

View File

@ -0,0 +1,34 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import {getConfigValue} from "@brojs/cli";
import { BASE_API_URL } from "../api.js";
const baseQuery = fetchBaseQuery({
baseUrl: BASE_API_URL,
prepareHeaders: (headers) => {
const token = localStorage.getItem('token');
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
return headers;
},
});
export const apiSlice = createApi({
reducerPath: 'api',
baseQuery,
endpoints: (builder) => ({
getChats: builder.query({
query: (username) => `/chat/list/${username}`,
}),
postChat: builder.mutation({
query: ({ id1, id2 }) => ({
url: `/chat/item/${id1}/${id2}`,
method: 'POST',
}),
}),
}),
});
// Export hooks for usage in functional components
export const { useGetChatsQuery, usePostChatMutation } = apiSlice;

View File

@ -0,0 +1,12 @@
import { configureStore } from '@reduxjs/toolkit';
import { apiSlice } from './api_slice';
const store = configureStore({
reducer: {
[apiSlice.reducerPath]: apiSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(apiSlice.middleware),
});
export default store;

31
src/backend/server.js Normal file
View File

@ -0,0 +1,31 @@
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
require("dotenv").config();
const app = express();
const server = http.createServer(app);
const io = new Server(server);
io.on("connection", (socket) => {
console.log("New connection:", socket.id);
// For messages
socket.on("sendMessage", (message) => {
console.log("Message received:", message);
socket.broadcast.emit("receiveMessage", message);
});
socket.on("disconnect", () => {
console.log("User disconnected:", socket.id);
});
});
app.get("/", (req, res) => {
res.send("Socket.IO Server is running");
});
const PORT = process.env.PORT || 8099;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

View File

@ -0,0 +1,51 @@
import React, {useState} from 'react';
import { URLs } from "../../__data__/urls";
import ActionButton from "./ActionButton.jsx";
import InputField from "../reg/InputField.jsx";
const AccountButtons = (props) => {
const [chName, setChName] = useState(false);
const [chPassword, setChPassword] = useState(false);
const [nickname, setNickname] = useState("");
const [password, setPassword] = useState("");
return (
<div className="account-buttons">
{props.registered ? (
<>
<ActionButton title={"Exit"} action={props.exitHandler}/>
<ActionButton title={"Change Name"} action={() => setChName(true)}/>
{chName ? (
<InputField
title={""}
value={nickname}
setValue={setNickname}
placeholder='Enter your new nickname'
submit={nickname}
enter={props.changeNameHandler}
/>
) : null}
<ActionButton title={"Change Pass"} action={() => setChPassword(true)}/>
{chPassword ? (
<div>
<InputField
title={""}
value={password}
setValue={setPassword}
placeholder='Enter your new password'
submit={password}
enter={props.changePassHandler}
/>
</div>
) : null}
</>
) : null}
<a className="MyButton mclaren-regular" href={URLs.home.url}>Back</a>
</div>
);
};
export default AccountButtons;

View File

@ -0,0 +1,11 @@
import React from 'react';
const ActionButton = (props) => {
return (
<a className="MyButton mclaren-regular" onClick={() => {
props.action()
}}>{props.title}</a>
);
};
export default ActionButton;

View File

@ -0,0 +1,18 @@
import React from 'react';
const HelloItem = (props) => {
return (
<div className="hello-item-class">
{!!props.nickname ? (
<>
<h1 className="mclaren-regular">Hello, {props.nickname}!</h1>
<p className="mclaren-regular">Your ID: {props.id}</p>
</>
) : (
<p className="mclaren-regular">You don't have an account :(</p>
)}
</div>
);
};
export default HelloItem;

View File

@ -0,0 +1,54 @@
.account-buttons {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 30px;
}
.account-buttons a {
display: flex;
font-size: 1.5vw;
transition: color 0.2s ease-in;
width: 20vw;
margin-top: 2vw;
}
.account-buttons a:hover {
color: black;
}
.hello-item-class {
display: flex;
flex-direction: column;
align-items: center;
}
.hello-item-class h1 {
font-size: 4vw;
margin-bottom: 0;
}
.hello-item-class p {
font-size: 1.5vw;
}
@media only screen and (max-width: 800px) {
.account-buttons a {
font-size: 2.5vh;
width: 60vw;
margin-top: 3vh;
}
.hello-item-class h1 {
font-size: 5vh;
}
.hello-item-class p {
font-size: 2vh;
}
}

View File

@ -0,0 +1,81 @@
import React from 'react';
import { URLs } from "../../__data__/urls";
import styled from "styled-components";
const Card = (props) => {
const interlocutorId = props.id
const hexToRgb = (hex) => {
hex = hex.replace(/^#/, '');
let r = parseInt(hex.slice(0, 2), 16);
let g = parseInt(hex.slice(2, 4), 16);
let b = parseInt(hex.slice(4, 6), 16);
return `${r}, ${g}, ${b}`;
}
const handleClick = (event, id) => {
localStorage.setItem('interlocutorId', id);
};
const StyledCard = styled.div`
box-shadow: 0 10px 40px rgba(176, 175, 189, 0.85);
padding: 2vw;
display: flex;
flex-direction: column;
border-radius: 2vw;
align-items: flex-start;
justify-content: space-between;
background-color: ${props => props.color
? `rgba(${hexToRgb(props.color)}, 0.63)`
: 'rgba(255, 255, 255, 0.63)'};
transition: box-shadow 0.3s ease-in;
div, h2 {
transition: color 0.3s ease-in;
}
h2 {
margin-top: 0;
}
&:hover {
box-shadow: 0 10px 40px rgb(${hexToRgb(props.color)});
cursor: pointer;
div, h2 {
color: white;
}
}
@media only screen and (max-width: 800px) {
flex-direction: column;
border-radius: 2vh;
padding: 5vw;
}
`
return (
<a
href={URLs.chat.url}
className="ChatCard"
onClick={(event) => handleClick(event, interlocutorId)}
>
<StyledCard color={props.color}>
<h2>{props.name}</h2>
<div className="lastMessageStyle">{props.lastMessage}</div>
</StyledCard>
</a>
);
};
export default Card;

View File

@ -0,0 +1,93 @@
import React, { useEffect, useState } from "react";
import Card from "./Card.jsx";
import { get } from "../../backend/api";
import { displayMessage } from "../../backend/notifications/notifications";
import { MessageType } from "../../backend/notifications/message";
const ChatsList = (props) => {
const { chats } = props;
const [customChats, setCustomChats] = useState([]);
const updateList = async () => {
const username = localStorage.getItem("username");
if (!username) return null;
const updatedChats = await Promise.all(
chats.map(async (chat) => {
const interlocutorId = chat.id1 === username ? chat.id2 : chat.id1;
const { ok, data } = await get("/auth/" + interlocutorId);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return null;
}
const interlocutor = data.user;
const lastMessage =
chat.messages.length > 0
? chat.messages[chat.messages.length - 1]
: { data: "", timestamp: new Date(0).toISOString() };
return {
id: interlocutorId,
name: interlocutor.nickname,
lastMessageData: lastMessage.data,
lastMessageTimestamp: lastMessage.timestamp,
};
})
);
const validChats = updatedChats.filter((chat) => chat !== null);
validChats.sort(
(a, b) =>
new Date(b.lastMessageTimestamp) - new Date(a.lastMessageTimestamp)
);
setCustomChats(validChats);
};
useEffect(() => {
updateList().then();
}, [chats]);
const colorMap = {
orange: "FFA500FF",
aqua: "00FFFFFF",
crimson: "DC143CFF",
red: "FF0000FF",
violet: "8A2BE2FF",
seagreen: "20B2AAFF",
green: "ADFF2FFF",
blue: "0000FFFF",
pink: "FF1493FF",
cyan: "72FAFAFF",
};
function getColor(chatId) {
const keys = Object.keys(colorMap);
const numericId = Array.from(chatId).reduce(
(sum, char) => sum + char.charCodeAt(0),
0
);
const index = numericId % keys.length;
return colorMap[keys[index]];
}
return (
<div className="ChatsList">
{customChats.map((item, index) => (
<Card
key={index}
name={item.name}
lastMessage={item.lastMessageData}
id={item.id}
color={getColor(item.id)}
/>
))}
</div>
);
};
export default ChatsList;

View File

@ -0,0 +1,14 @@
import React from 'react';
import { URLs } from "../../__data__/urls";
import { FaHome } from 'react-icons/fa';
const Header = () => {
return (
<a href={URLs.account.url} className="accountLink">
<FaHome className="houseIcon"/>
<p>My profile</p>
</a>
);
};
export default Header;

View File

@ -0,0 +1,14 @@
import React from 'react';
import chatIcon from './../../../images/chat.svg';
import { URLs } from "../../__data__/urls";
const HomeTitle = () => {
return (
<a className="homeTitleWrapper" href={URLs.baseUrl}>
<h1 className="montecarlo-regular homeTitle">Enterfront</h1>
<img src={chatIcon} alt="Chat Icon" className="chatIcon"/>
</a>
);
};
export default HomeTitle;

View File

@ -0,0 +1,11 @@
import React from 'react';
const Search = (props) => {
return (
<a className="MyButton search-class mclaren-regular" onClick={() => {
props.search(props.item);
}}>Find</a>
);
};
export default Search;

View File

@ -0,0 +1,38 @@
.houseIcon {
transition: color 0.3s ease-in;
}
.accountLink {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.accountLink p {
font-size: 1.5vw;
transition: color 0.3s ease-out;
}
.accountLink svg {
font-size: 3vw;
transition: fill 0.3s ease-in;
}
.accountLink:hover p {
color: white;
}
.accountLink:hover svg {
fill: orange;
}
@media only screen and (max-width: 800px) {
.accountLink p {
font-size: 1.8vh;
}
.accountLink svg {
font-size: 4vh;
}
}

View File

@ -0,0 +1,48 @@
.ChatsList {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 3rem;
}
.ChatCard {
margin-bottom: 40px;
max-width: 30vw;
min-width: 20vw;
margin-left: 1.2vw;
margin-right: 1.2vw;
border-radius: 2vw;
transition: transform 0.2s ease-in;
}
.lastMessageStyle {
width: 80%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media only screen and (max-width: 800px) {
.ChatCard {
width: 30vw;
max-width: 30vw;
margin-left: 4vw;
margin-right: 4vw;
padding-top: 0;
}
.ChatsList {
margin: 10vw;
}
}
.ChatCard:hover {
transform: translate(2px, -10px);
}

View File

@ -0,0 +1,44 @@
.homeTitleWrapper {
display: flex;
flex-direction: row;
}
.homeTitle {
font-size: 5vw;
transition: color 0.3s ease-in;
}
.homeTitleWrapper:hover h1 {
color: orange;
}
.search-class {
margin-top: 2vw;
margin-bottom: 4vw;
display: flex;
align-items: center;
}
.search-input div input {
background-color: white;
color: black;
border: 3px solid black;
}
@media only screen and (max-width: 800px) {
.homeTitle {
font-size: 8vh;
}
.search-class {
margin-top: 3vh;
}
}
.chatIcon {
width: 6vw;
margin-left: 3vw;
}

View File

@ -0,0 +1,3 @@
@import url("./css/card.css");
@import url("./css/homeTitle.css");
@import url("./css/accountLink.css");

28
src/components/index.css Normal file
View File

@ -0,0 +1,28 @@
@import url("reg/index.css");
@import url("init/index.css");
@import url("home/index.css");
@import url("account/index.css");
.MyButton {
text-decoration: none;
border: 2px solid black;
border-radius: 10px;
background-color: black;
color: white;
padding: 15px;
justify-content: center;
width: 9rem;
transition: background-color 0.2s ease-in, border 0.2s ease-in;
}
.MyButton:hover {
background-color: orange;
border: 2px solid orange;
cursor: pointer;
}

View File

@ -0,0 +1,9 @@
import React from 'react';
const InitTitle = () => {
return (
<h1 className="metal-mania-regular initTitle">Enterfront</h1>
);
};
export default InitTitle;

View File

@ -0,0 +1,12 @@
import React from 'react';
const NavButton = (props) => {
return (
<a className="MyButton singleNavButton" href={props.nav}>
<p className="mclaren-regular">{props.text}</p>
</a>
);
};
export default NavButton;

View File

@ -0,0 +1,13 @@
.initTitle {
font-size: 7vw;
margin: 0;
color: white;
text-align: center;
}
@media only screen and (max-width: 800px) {
.initTitle {
font-size: 8vh;
}
}

View File

@ -0,0 +1,32 @@
.MyButton.singleNavButton {
display: flex;
align-items: center;
margin-top: 1.5vw;
padding: 0;
background-color: white;
border-color: white;
border-radius: 3vw;
min-width: 20vw;
}
.MyButton.singleNavButton p {
font-size: 1.4vw;
font-weight: bold;
}
@media only screen and (max-width: 800px) {
.MyButton.singleNavButton p {
font-size: 2.5vh;
}
.MyButton.singleNavButton {
min-width: 60vw;
border-radius: 30vh;
margin-top: 3vh;
}
}

View File

@ -0,0 +1,2 @@
@import url("./css/navButton.css");
@import url("./css/initTitle.css");

View File

@ -0,0 +1,25 @@
import React from 'react';
const InputField = (props) => {
console.log('class:', props.className)
return (
<div>
<p>{props.title}</p>
<input
onChange={(e) => props.setValue(e.target.value)}
value={props.value}
className="Input"
placeholder={(props.placeholder) ? props.placeholder : ''}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (props.submit) {
props.enter(props.submit);
}
}
}}
/>
</div>
);
};
export default InputField;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { URLs } from "../../__data__/urls";
const LoginButtons = (props) => {
const ref = props.isAuth ? URLs.reg.url : URLs.auth.url
return (
<div>
<div className="LoginButtons">
<a className="MyButton loginButton" onClick={() => (
props.submitHandler()
)}>
{props.isAuth
? <p className="mclaren-regular">Login</p>
: <p className="mclaren-regular">Register</p>}
</a>
<a className="MyButton loginButton" href={ref}>
{props.isAuth
? <p className="mclaren-regular">Registration</p>
: <p className="mclaren-regular">I have an account</p>}
</a>
</div>
</div>
);
};
export default LoginButtons;

View File

@ -0,0 +1,58 @@
.LoginButtons {
margin-top: 5rex;
display: flex;
flex-direction: column;
align-items: center;
}
.LoginButtons a {
margin-bottom: 15px;
width: 12rem;
display: flex;
}
.Input {
border: 0;
background-color: black;
color: orange;
border-radius: 5px;
padding: 10px;
width: 18rem;
}
.MyButton.loginButton {
padding: 0;
}
.MyButton.loginButton p {
color: orange;
transition: color 0.3s ease-in;
font-size: 1.3vw;
font-weight: 500;
}
.loginTitle {
font-size: 4vw;
transition: color 0.3s ease-in;
}
.loginTitle:hover {
color: orange;
}
@media only screen and (max-width: 800px) {
.MyButton.loginButton p {
font-size: 1.5vh;
}
}
.loginButton:hover p {
color: black;
}

View File

@ -0,0 +1 @@
@import url("./css/login.css");

View File

@ -0,0 +1,12 @@
import React from 'react';
import { URLs } from "../../__data__/urls";
const LoginTitle = () => {
return (
<a href={URLs.baseUrl}>
<h1 className="loginTitle mclaren-regular">Enterfront</h1>
</a>
);
};
export default LoginTitle;

View File

@ -1,18 +1,25 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import React from "react";
import { Routes, Route } from "react-router-dom";
import { URLs } from './__data__/urls';
import { URLs } from "./__data__/urls";
import HomePage from './pages/HomePage.jsx'
import Hello from './pages/Hello.jsx'
import Account from './pages/Account.jsx'
import Home from "./pages/Home.jsx";
import Init from "./pages/Init.jsx";
import Account from "./pages/Account.jsx";
import Chat from "./pages/Chat.jsx";
import SignIn from "./pages/SignIn.jsx";
import SignUp from "./pages/SignUp.jsx";
export const Dashboard = () => {
return (
<Routes>
<Route path={URLs.baseUrl} element={<HomePage/>}/>
<Route path={URLs.auth.url} element={<Hello/>} />
<Route path={URLs.account.url} element={<Account/>}/>
<Route path={URLs.baseUrl} element={<Init />} />
<Route path={URLs.home.url} element={<Home />} />
<Route path={URLs.chat.url} element={<Chat />} />
<Route path={URLs.auth.url} element={<SignIn />} />
<Route path={URLs.reg.url} element={<SignUp />} />
<Route path={URLs.account.url} element={<Account />} />
<Route path="*" element={<h1>404 page not found</h1>} />
</Routes>
);
};

60
src/index.css Normal file
View File

@ -0,0 +1,60 @@
@import url('https://fonts.googleapis.com/css2?family=MonteCarlo&family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Metal+Mania&display=swap');
@import url('https://fonts.googleapis.com/css2?family=McLaren&display=swap');
@import url('https://fonts.googleapis.com/css2?family=MonteCarlo&display=swap');
@import url("pages/index.css");
@import url("components/index.css");
body {
margin: 0;
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;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
* {
font-size: 1.2vw;
font-family: "Montserrat", 'sans-serif';
color: black;
text-decoration: none;
}
/* Mobile devices */
@media only screen and (max-width: 800px) {
* {
font-size: 1.5vh;
}
}
html {
scroll-behavior: smooth;
}
.metal-mania-regular {
font-family: "Metal Mania", system-ui;
font-weight: 400;
font-style: normal;
}
.mclaren-regular {
font-family: "McLaren", system-ui;
font-weight: 400;
font-style: normal;
}
.montecarlo-regular {
font-family: "MonteCarlo", cursive;
font-weight: 400;
font-style: normal;
}

View File

@ -1,9 +1,81 @@
import React from "react";
import React, {useEffect, useState} from "react";
import AccountButtons from "../components/account/AccountButtons.jsx";
import userIcon from "../../images/user.svg";
import {get, post} from "../backend/api";
import {displayMessage} from "../backend/notifications/notifications";
import {MessageType} from "../backend/notifications/message";
import HelloItem from "../components/account/HelloItem.jsx";
import { URLs } from "../__data__/urls";
const Account = () => {
return (
<h1>Account</h1>
)
}
const exitHandler = () => {
localStorage.removeItem("username");
localStorage.removeItem("token");
export default Account;
localStorage.setItem("message", "Exited successfully!");
window.location.href = URLs.baseUrl;
}
const [nickname, setNickname] = useState("");
const [id, setId] = useState("");
async function changeNameHandler (newNickname) {
if (!newNickname) return;
const {ok, data} = await post('/change/nickname', {id: id, newNickname: newNickname});
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
} else {
localStorage.setItem("message", "Name was changed");
window.location.href = URLs.account.url;
}
}
async function changePassHandler (newPass){
if (!newPass) return;
const {ok, data} = await post('/change/password', {id: id, newPassword: newPass});
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
} else {
localStorage.setItem("message", "Password was changed");
window.location.href = URLs.account.url;
}
}
async function getUser() {
const username = localStorage.getItem("username");
if (!username) {
displayMessage("You're not logged in!", MessageType.WARN);
return;
}
const {ok, data} = await get('/auth/' + username);
if (!ok) {
displayMessage("Some error with auth:" + data.message, MessageType.ERROR);
return;
}
setNickname(data.user.nickname);
setId(username);
}
useEffect(() => {getUser().then()}, [])
return (
<div className="account-items">
<img src={userIcon} alt="user" />
<HelloItem nickname={nickname} id={id} />
<AccountButtons
exitHandler={exitHandler}
changeNameHandler={changeNameHandler}
changePassHandler={changePassHandler}
registered={!!nickname}
/>
</div>
);
};
export default Account;

243
src/pages/Chat.jsx Normal file
View File

@ -0,0 +1,243 @@
import React, { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import "./css/Chat.css";
import { FaPaperPlane, FaSmile } from "react-icons/fa";
import { get, post } from "../backend/api";
import { displayMessage } from "../backend/notifications/notifications";
import { MessageType } from "../backend/notifications/message";
import io from "socket.io-client";
const emojis = [
"😀",
"😁",
"😂",
"🤣",
"😃",
"😄",
"😅",
"😆",
"😉",
"😊",
"😋",
"😎",
"😍",
"😘",
"🥰",
"😗",
"😙",
"😚",
"🙂",
"🤗",
"🤩",
"🤔",
"😐",
"😑",
"😶",
"🙄",
"😏",
"😣",
"😥",
"😮",
"🤐",
"😯",
"😪",
"😫",
"😴",
"😌",
"😛",
"😜",
"🤪",
"🤨",
"😝",
"🤑",
"😒",
"😓",
"😔",
"😕",
"😖",
"😞",
"😟",
"😠",
"😡",
"🤬",
"😱",
"😨",
"😧",
"😇",
"🥳",
"🥺",
"😻",
"😼",
"😽",
"🙈",
"🙉",
"🙊",
"💀",
"👻",
"👽",
];
const Chat = () => {
const [interlocutorId, setInterlocutorId] = useState("");
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const socket = useRef(null);
const chatRef = useRef(null);
const navigate = useNavigate();
const [myId, setMyId] = useState("");
useEffect(() => {
const id = localStorage.getItem("interlocutorId");
setInterlocutorId(id);
const username = localStorage.getItem("username");
setMyId(username);
if (!id || !username) {
displayMessage("You are not logged in!", MessageType.WARN);
return () => {};
}
socket.current = io("http://localhost:8099");
socket.current.on("receiveMessage", (message) => {
setMessages((prev) => [...prev, message]);
});
socket.current.on("connect_error", (err) => {
console.error("Connection Error:", err.message);
});
return () => {
socket.current.disconnect();
};
}, []);
useEffect(() => {
// retrieveMessages().then();
const interval = setInterval(() => {
retrieveMessages().then()
}, 2000);
return () => clearInterval(interval)
}, [myId, interlocutorId]);
useEffect(() => {
if (chatRef.current) {
chatRef.current.scrollTop = chatRef.current.scrollHeight;
}
}, [messages]);
async function sendMessageToDB(messageData) {
const { ok, data } = await post(
"/chat/message/" + myId + "/" + interlocutorId,
{ message: messageData }
);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
}
}
async function retrieveMessages() {
if (!myId || !interlocutorId) return;
const { ok, data } = await get("/chat/item/" + myId + "/" + interlocutorId);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return;
}
setMessages(data.chat.messages);
}
const sendMessage = () => {
if (newMessage.trim()) {
const messageData = {
senderId: myId,
recipientId: interlocutorId,
data: newMessage,
timestamp: new Date().toLocaleString(),
};
socket.current.emit("sendMessage", messageData);
setMessages((prev) => [...prev, messageData]);
sendMessageToDB(messageData).then();
setNewMessage("");
}
};
const handleKeyPress = (e) => {
if (e.key === "Enter") {
sendMessage();
}
};
const handleEmojiSelect = (emoji) => {
setNewMessage((prev) => prev + emoji);
setShowEmojiPicker(false);
};
return (
<div className="chat-container">
<div className="chat-header">
<h2>Chat with ... (id = {interlocutorId})</h2>
<button
onClick={() => navigate("/enterfront/home")}
className="home-button"
>
Home
</button>
</div>
<div className="chat-messages" ref={chatRef}>
{messages.map((msg, index) => (
<div
key={index}
className={`message-bubble ${
msg.senderId === myId ? "sent" : "received"
}`}
>
<div className="message-content">
<b>{msg.senderId === myId ? "You" : "They"}:</b> {msg.data}
</div>
<span className="message-timestamp">{msg.timestamp}</span>
</div>
))}
</div>
<div className="chat-input-container">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message..."
className="chat-input"
onKeyDown={handleKeyPress}
/>
<button
className="emoji-button"
onClick={() => setShowEmojiPicker((prev) => !prev)}
>
<FaSmile />
</button>
<button onClick={sendMessage} className="send-button">
<FaPaperPlane />
</button>
{showEmojiPicker && (
<div className="emoji-picker">
{emojis.map((emoji, index) => (
<span
key={index}
className="emoji"
onClick={() => handleEmojiSelect(emoji)}
style={{ cursor: "pointer", padding: "5px" }}
>
{emoji}
</span>
))}
</div>
)}
</div>
</div>
);
};
export default Chat;

View File

@ -1,9 +0,0 @@
import React from "react";
const Hello = () => {
return (
<h1>Hello!</h1>
)
}
export default Hello;

115
src/pages/Home.jsx Normal file
View File

@ -0,0 +1,115 @@
import React, { useEffect, useState } from "react";
import HomeTitle from "../components/home/HomeTitle.jsx";
import ChatsList from "../components/home/ChatsList.jsx";
import Header from "../components/home/Header.jsx";
import { displayMessage } from "../backend/notifications/notifications";
import { MessageType } from "../backend/notifications/message";
import { useGetChatsQuery, usePostChatMutation } from "../backend/redux/api_slice"; // Update the import based on your API slice
import InputField from "../components/reg/InputField.jsx";
import Search from "../components/home/Search.jsx";
import { URLs } from "../__data__/urls";
const Home = () => {
const [chats, setChats] = useState([]); // Retained original variable name
const [interlocutor, setInterlocutor] = useState("");
const username = localStorage.getItem("username");
// Use Redux Queries
const { data: chatsData, error: getError, isLoading: isGetting } = useGetChatsQuery(username, {
skip: !username
});
console.log('From Redux:', chatsData);
const [createChat, { error: postError }] = usePostChatMutation();
useEffect(() => {
if (getError) {
displayMessage(getError.message, MessageType.ERROR);
}
if (getError) {
displayMessage(getError.message, MessageType.ERROR);
}
}, [getError, postError]);
useEffect(() => {
if (chatsData) {
// setChats(chatsData.chats);
let data = chatsData.chats;
try {
const sortedChats = [...data].sort((a, b) => {
const lastMessageA = a.messages[a.messages.length - 1];
const lastMessageB = b.messages[b.messages.length - 1];
const dateA = new Date(lastMessageA.timestamp);
const dateB = new Date(lastMessageB.timestamp);
return dateB - dateA;
});
setChats(sortedChats);
} catch (e) {
console.error(e);
}
}
}, [chatsData]);
const createChatHandler = async (alias) => {
if (!username) {
displayMessage("You're not logged in!", MessageType.WARN);
return;
}
displayMessage("Sent", MessageType.INFO);
try {
const data = await createChat({ id1: alias, id2: username }).unwrap(); // Using unwrap to handle promise rejection
localStorage.setItem("message", "Successfully opened chat!");
localStorage.setItem("interlocutorId", alias);
window.location.href = URLs.chat.url;
} catch (error) {
displayMessage(error.data.message, MessageType.ERROR);
}
};
return (
<div className="homeWrapper">
<div className="headerPos">
<Header />
</div>
<HomeTitle />
<div className="search-input">
<InputField
title="Create new chat"
value={interlocutor}
setValue={setInterlocutor}
placeholder="Enter the username (id)"
enter={createChatHandler}
submit={interlocutor}
/>
</div>
{isGetting ? (
<div>Loading...</div>
) : (
<>
<Search search={createChatHandler} item={interlocutor} />
<p>Your chats</p>
<ChatsList chats={chats} />
</>
)}
</div>
);
};
export default Home;

View File

@ -1,9 +0,0 @@
import React from "react";
const HomePage = () => {
return (
<h1>Home</h1>
)
}
export default HomePage

22
src/pages/Init.jsx Normal file
View File

@ -0,0 +1,22 @@
import React from "react";
import NavButton from "../components/init/NavButton.jsx";
import {URLs} from "../__data__/urls";
import InitTitle from "../components/init/InitTitle.jsx";
const Init = () => {
return (
<div className="backgroundImage">
<div className="blurEffect"></div>
<div className="overBlur initTitlePos">
<InitTitle/>
</div>
<div className="navButtons overBlur">
<NavButton nav={URLs.home.url} text={"Start chatting"}/>
<NavButton nav={URLs.auth.url} text={"Sign In"}/>
<NavButton nav={URLs.reg.url} text={"Sign Up"}/>
</div>
</div>
)
}
export default Init;

58
src/pages/SignIn.jsx Normal file
View File

@ -0,0 +1,58 @@
import React, {useState} from 'react';
import InputField from "../components/reg/InputField.jsx";
import LoginButtons from "../components/reg/LoginButtons.jsx";
import LoginTitle from "../components/reg/loginTitle.jsx";
import {MessageType} from "../backend/notifications/message.tsx";
import {displayMessage} from "../backend/notifications/notifications.js";
import {post} from "../backend/api.js";
import {URLs} from "../__data__/urls";
const SignIn = () => {
const [name, setName] = useState("");
const [password, setPassword] = useState("");
const [nameErrorsCounter, setNameErrorsCounter] = useState(0);
async function submit() {
console.log('Sign In!')
const {ok, data} = await post('/auth/login', {name: name, password: password});
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
if (nameErrorsCounter >= 1) {
displayMessage("Note that you need to enter your ID name", MessageType.INFO);
setNameErrorsCounter(0);
} else {
setNameErrorsCounter(nameErrorsCounter + 1);
}
return;
}
localStorage.setItem('token', data.token);
localStorage.setItem('username', name);
setNameErrorsCounter(0);
localStorage.setItem('message', 'Successfully logged in!');
window.location.href = URLs.baseUrl;
}
return (
<div className="LoginList">
<LoginTitle/>
<InputField title="Name" setValue={setName} value={name}/>
<InputField title="Password" setValue={setPassword} value={password}/>
<LoginButtons
submitHandler={submit}
isAuth={true}
/>
</div>
);
};
export default SignIn;

70
src/pages/SignUp.jsx Normal file
View File

@ -0,0 +1,70 @@
import React, {useState} from 'react';
import InputField from "../components/reg/InputField.jsx";
import LoginButtons from "../components/reg/LoginButtons.jsx";
import LoginTitle from "../components/reg/loginTitle.jsx";
import {post} from "../backend/api";
import {displayMessage} from "../backend/notifications/notifications";
import {MessageType} from "../backend/notifications/message";
import { URLs } from "../__data__/urls";
const SignUp = () => {
const [name, setName] = useState("");
const [nickname, setNickname] = useState("");
const [password, setPassword] = useState("");
const [repeatPassword, setRepeatPassword] = useState("");
async function login(name, password) {
const {ok, data} = await post('/auth/login', {name: name, password: password});
return {loginStatus: ok, loginData: data};
}
async function submit () {
console.log('Sign Up!');
if (password !== repeatPassword) {
displayMessage("Passwords don't match", MessageType.WARN);
return;
}
const {ok, data} = await post('/auth/reg',
{name: name, password: password, nickname: nickname});
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return;
}
const { loginStatus, loginData } = await login(name, password);
console.log(loginStatus, loginData)
if (!loginStatus) {
displayMessage(loginData.message, MessageType.ERROR);
return;
}
localStorage.setItem('token', loginData.token);
localStorage.setItem('username', name);
localStorage.setItem('message', 'Successfully signed up!');
window.location.href = URLs.baseUrl;
}
return (
<div className="LoginList">
<LoginTitle/>
<InputField title="Name" setValue={setName} value={name}/>
<InputField title="Nickname (for others)" setValue={setNickname} value={nickname}/>
<InputField title="Password" setValue={setPassword} value={password}/>
<InputField title="Repeat Password" setValue={setRepeatPassword} value={repeatPassword}/>
<LoginButtons
submitHandler={submit}
isAuth={false}
/>
</div>
);
};
export default SignUp;

160
src/pages/css/Chat.css Normal file
View File

@ -0,0 +1,160 @@
body {
background: linear-gradient(to right, #e0f7fa, #fffde7);
}
.chat-container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 600px;
margin: auto;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
background-color: #f9f9f9;
}
.chat-header {
padding: 15px;
background-color: #007bff;
color: white;
text-align: center;
font-weight: bold;
letter-spacing: 1px;
border-bottom: 2px solid #0056b3;
}
.chat-messages {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 10px;
overflow-y: auto;
background-color: #fff;
max-height: 400px;
}
.message-bubble {
margin: 5px;
padding: 10px 15px;
border-radius: 10px;
max-width: 70%;
word-wrap: break-word;
display: inline-block;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.message-bubble:hover {
transform: translateY(-2px);
}
.sent {
background-color: #007bff;
color: white;
align-self: flex-end;
text-align: right;
margin-left: auto;
}
.received {
background-color: #f1f0f0;
align-self: flex-start;
text-align: left;
margin-right: auto;
}
.message-content {
font-size: 14px;
margin: 0;
padding: 0;
}
.message-timestamp {
font-size: 10px;
color: black;
text-align: right;
margin-top: 4px;
display: block;
}
.chat-input-container {
display: flex;
padding: 10px;
border-top: 1px solid #ddd;
background-color: #f0f0f0;
}
.chat-input {
flex-grow: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 20px;
font-size: 14px;
outline: none;
}
.send-button,
.emoji-button {
background-color: transparent;
border: none;
font-size: 20px;
margin-left: 10px;
cursor: pointer;
transition: color 0.3s;
}
.send-button:hover,
.emoji-button:hover {
color: #0056b3;
}
.send-button {
color: #007bff;
}
.emoji-button {
color: #f0c040;
}
.chat-input:focus {
border-color: #007bff;
}
.emoji-picker {
position: absolute;
bottom: 60px;
right: 10px;
z-index: 10;
background-color: white;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
display: flex;
flex-wrap: wrap;
max-width: 200px;
}
.emoji {
font-size: 24px;
cursor: pointer;
transition: transform 0.2s;
}
.emoji:hover {
transform: scale(1.2);
}
.home-button {
background-color: #4caf50;
color: white;
padding: 10px 15px;
border: none;
border-radius: 5px;
cursor: pointer;
margin-left: auto;
}
.home-button:hover {
background-color: #45a049;
}

12
src/pages/css/account.css Normal file
View File

@ -0,0 +1,12 @@
.account-items {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 5vw;
}
.account-items img {
margin-bottom: 1vw;
}

31
src/pages/css/home.css Normal file
View File

@ -0,0 +1,31 @@
.homeWrapper {
display: flex;
flex-direction: column;
align-items: center;
}
.homeWrapper p {
font-size: 2.5vw;
font-weight: 500;
margin-top: 0;
}
@media only screen and (max-width: 800px) {
.homeWrapper p {
font-size: 3vh;
}
}
.ChatsList {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: 3rem;
}
.headerPos {
position: absolute;
top: 3vw;
right: 3vw;
}

45
src/pages/css/init.css Normal file
View File

@ -0,0 +1,45 @@
.backgroundImage {
width: 100%;
height: 100vh;
background-color: darkgray;
background-size: cover;
background-position: center;
position: relative;
}
.blurEffect {
width: 100%;
height: 100%;
background-image: url("./../../../images/home_wp.jpg");
background-size: cover;
background-position: center;
filter: blur(10px);
position: absolute;
top: 0;
left: 0;
z-index: 1;
}
.overBlur {
position: relative;
z-index: 2;
}
.initTitlePos {
top: 10%;
}
.navButtons {
display: flex;
flex-direction: column;
align-items: center;
top: 17%;
}

8
src/pages/css/input.css Normal file
View File

@ -0,0 +1,8 @@
.LoginList {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-top: 2rem;
}

4
src/pages/index.css Normal file
View File

@ -0,0 +1,4 @@
@import url("css/init.css");
@import url("css/home.css");
@import url("css/input.css");
@import url("css/account.css");

73
stubs/api/auth/index.js Normal file
View File

@ -0,0 +1,73 @@
const authRouter = require('express').Router();
// For creating tokens
const jwt = require('jsonwebtoken');
const { TOKEN_KEY } = require('../key')
module.exports = authRouter;
const { addUserToDB, getUserFromDB } = require('../db');
// Get a user by its id
authRouter.get('/:id', (req, res) => {
const user = getUserFromDB(req.params.id);
if (user) {
res.status(200).send({user});
} else {
res.status(404).send({message: 'User was not found'});
}
})
// For login (authorization)
authRouter.post('/login', (req, res) => {
const { name, password } = req.body;
const user = getUserFromDB(name);
// Invalid identification
if (!user) {
res.status(401).send({message: 'Invalid credentials (id)'});
return;
}
// Invalid authentication
if (!password || password !== user.password) {
res.status(401).send({message: 'Invalid credentials (password)'});
return;
}
// Now, authorization
const token = jwt.sign({id: name}, TOKEN_KEY, {
expiresIn: '1h'
})
res.status(200).send({token});
})
authRouter.post('/reg', (req, res) => {
const { name, password, nickname } = req.body;
const user = getUserFromDB(name);
// Invalid identification
if (user) {
res.status(409).send({message: 'Such id already exists'});
return;
}
if (!name || !password || !nickname) {
res.status(401).send({message: 'Empty or invalid fields'});
return;
}
// Add to 'DB'
const newUser = {id: name, password: password, nickname: nickname};
addUserToDB(newUser)
res.status(200).send({user: newUser});
})

52
stubs/api/auth/users.json Normal file
View File

@ -0,0 +1,52 @@
[
{
"nickname": "Alice Johnson",
"password": "1234",
"id": "alice"
},
{
"nickname": "Bob Smith",
"password": "1234",
"id": "bobsm"
},
{
"nickname": "Charlie Brown",
"password": "1234",
"id": "charl"
},
{
"nickname": "David Clark",
"password": "1234",
"id": "david"
},
{
"nickname": "Eve Adams",
"password": "1234",
"id": "evead"
},
{
"nickname": "Frank Wright",
"password": "1234",
"id": "frank"
},
{
"nickname": "Grace Lee",
"password": "1234",
"id": "grace"
},
{
"nickname": "Hannah Scott",
"password": "1234",
"id": "hanna"
},
{
"nickname": "Ian Davis",
"password": "1234",
"id": "ianda"
},
{
"nickname": "Jill Thompson",
"password": "1234",
"id": "jillt"
}
]

64
stubs/api/change/index.js Normal file
View File

@ -0,0 +1,64 @@
const changeRouter = require('express').Router();
module.exports = changeRouter;
const { getUserFromDB, deleteUserFromDB, addUserToDB } = require('../db');
changeRouter.post('/nickname', (req, res) => {
const { id, newNickname } = req.body;
const user = getUserFromDB(id);
// Invalid identification
if (!user) {
res.status(401).send({message: 'Invalid credentials (id)'});
return;
}
const updatedUser = {
"nickname": newNickname,
"password": user.password,
"id": user.id
};
// Delete the old one
deleteUserFromDB(id)
// Insert updated
addUserToDB(updatedUser);
res.status(200).send({});
});
changeRouter.post('/password', (req, res) => {
const { id, newPassword } = req.body;
const user = getUserFromDB(id);
// Invalid identification
if (!user) {
res.status(401).send({message: 'Invalid credentials (id)'});
return;
}
// Delete the old one
deleteUserFromDB(id)
// Insert updated
const updatedUser = {
"nickname": user.nickname,
"password": newPassword,
"id": user.id
};
addUserToDB(updatedUser);
res.status(200).send({});
});
changeRouter.delete('/:id', (req, res) => {
const { id } = req.params;
deleteUserFromDB(id);
});

662
stubs/api/chat/chats.json Normal file
View File

@ -0,0 +1,662 @@
[
{
"id1": "alice",
"id2": "bobsm",
"messages": [
{
"data": "Hello Bob!",
"senderId": "alice",
"recipientId": "bobsm",
"timestamp": "09.10.2024 07:00:00"
},
{
"data": "Hey Alice, how are you?",
"senderId": "bobsm",
"recipientId": "alice",
"timestamp": "09.10.2024 07:05:00"
},
{
"data": "I'm good, thanks for asking.",
"senderId": "alice",
"recipientId": "bobsm",
"timestamp": "09.10.2024 07:10:00"
},
{
"data": "Glad to hear!",
"senderId": "bobsm",
"recipientId": "alice",
"timestamp": "09.10.2024 07:15:00"
}
]
},
{
"id1": "alice",
"id2": "charl",
"messages": [
{
"data": "How's the project going?",
"senderId": "alice",
"recipientId": "charl",
"timestamp": "09.10.2024 07:20:00"
},
{
"data": "It's coming along, almost done!",
"senderId": "charl",
"recipientId": "alice",
"timestamp": "09.10.2024 07:25:00"
},
{
"data": "That's great to hear!",
"senderId": "alice",
"recipientId": "charl",
"timestamp": "09.10.2024 07:30:00"
},
{
"data": "Thanks for checking in.",
"senderId": "charl",
"recipientId": "alice",
"timestamp": "09.10.2024 07:35:00"
}
]
},
{
"id1": "alice",
"id2": "david",
"messages": [
{
"data": "Did you get the files?",
"senderId": "david",
"recipientId": "alice",
"timestamp": "09.10.2024 07:40:00"
},
{
"data": "Yes, I did. Thank you!",
"senderId": "alice",
"recipientId": "david",
"timestamp": "09.10.2024 07:45:00"
},
{
"data": "You're welcome.",
"senderId": "david",
"recipientId": "alice",
"timestamp": "09.10.2024 07:50:00"
},
{
"data": "Let me know if you need anything else.",
"senderId": "alice",
"recipientId": "david",
"timestamp": "09.10.2024 07:55:00"
}
]
},
{
"id1": "alice",
"id2": "frank",
"messages": [
{
"data": "Can you review this document for me?",
"senderId": "alice",
"recipientId": "frank",
"timestamp": "09.10.2024 08:20:00"
},
{
"data": "Sure, I'll take a look.",
"senderId": "frank",
"recipientId": "alice",
"timestamp": "09.10.2024 08:25:00"
},
{
"data": "Thanks, much appreciated!",
"senderId": "alice",
"recipientId": "frank",
"timestamp": "09.10.2024 08:30:00"
},
{
"data": "No problem.",
"senderId": "frank",
"recipientId": "alice",
"timestamp": "09.10.2024 08:35:00"
}
]
},
{
"id1": "alice",
"id2": "grace",
"messages": [
{
"data": "Hey Grace, let's meet up for coffee!",
"senderId": "alice",
"recipientId": "grace",
"timestamp": "09.10.2024 08:40:00"
},
{
"data": "Sounds good, when are you free?",
"senderId": "grace",
"recipientId": "alice",
"timestamp": "09.10.2024 08:45:00"
},
{
"data": "How about tomorrow afternoon?",
"senderId": "alice",
"recipientId": "grace",
"timestamp": "09.10.2024 08:50:00"
},
{
"data": "Works for me!",
"senderId": "grace",
"recipientId": "alice",
"timestamp": "09.10.2024 08:55:00"
}
]
},
{
"id1": "alice",
"id2": "hanna",
"messages": [
{
"data": "Hannah, do you have a moment?",
"senderId": "alice",
"recipientId": "hanna",
"timestamp": "09.10.2024 09:00:00"
},
{
"data": "Sure, what's up?",
"senderId": "hanna",
"recipientId": "alice",
"timestamp": "09.10.2024 09:05:00"
},
{
"data": "Just wanted to check on the report.",
"senderId": "alice",
"recipientId": "hanna",
"timestamp": "09.10.2024 09:10:00"
},
{
"data": "I'll send it soon.",
"senderId": "hanna",
"recipientId": "alice",
"timestamp": "09.10.2024 09:15:00"
}
]
},
{
"id1": "alice",
"id2": "ianda",
"messages": [
{
"data": "Ian, have you completed the review?",
"senderId": "alice",
"recipientId": "ianda",
"timestamp": "09.10.2024 09:20:00"
},
{
"data": "Yes, I sent my feedback.",
"senderId": "ianda",
"recipientId": "alice",
"timestamp": "09.10.2024 09:25:00"
},
{
"data": "Thanks for that.",
"senderId": "alice",
"recipientId": "ianda",
"timestamp": "09.10.2024 09:30:00"
},
{
"data": "Anytime!",
"senderId": "ianda",
"recipientId": "alice",
"timestamp": "09.10.2024 09:35:00"
}
]
},
{
"id1": "alice",
"id2": "jillt",
"messages": [
{
"data": "Jill, let's schedule a catch-up meeting.",
"senderId": "alice",
"recipientId": "jillt",
"timestamp": "09.10.2024 09:40:00"
},
{
"data": "Sounds good, when works for you?",
"senderId": "jillt",
"recipientId": "alice",
"timestamp": "09.10.2024 09:45:00"
},
{
"data": "Tomorrow afternoon?",
"senderId": "alice",
"recipientId": "jillt",
"timestamp": "09.10.2024 09:50:00"
},
{
"data": "That works for me!",
"senderId": "jillt",
"recipientId": "alice",
"timestamp": "09.10.2024 09:55:00"
}
]
},
{
"id1": "alice",
"id2": "evead",
"messages": [
{
"data": "Eve, did you send the schedule?",
"senderId": "alice",
"recipientId": "evead",
"timestamp": "09.10.2024 10:00:00"
},
{
"data": "Yes, just sent it.",
"senderId": "evead",
"recipientId": "alice",
"timestamp": "09.10.2024 10:05:00"
},
{
"data": "Thanks, much appreciated!",
"senderId": "alice",
"recipientId": "evead",
"timestamp": "09.10.2024 10:10:00"
},
{
"data": "No problem!",
"senderId": "evead",
"recipientId": "alice",
"timestamp": "09.10.2024 10:15:00"
}
]
},
{
"id1": "bobsm",
"id2": "charl",
"messages": [
{
"data": "How's everything going?",
"senderId": "bobsm",
"recipientId": "charl",
"timestamp": "09.10.2024 10:20:00"
},
{
"data": "Pretty good, how about you?",
"senderId": "charl",
"recipientId": "bobsm",
"timestamp": "09.10.2024 10:25:00"
},
{
"data": "Can't complain!",
"senderId": "bobsm",
"recipientId": "charl",
"timestamp": "09.10.2024 10:30:00"
},
{
"data": "Glad to hear that.",
"senderId": "charl",
"recipientId": "bobsm",
"timestamp": "09.10.2024 10:35:00"
}
]
},
{
"id1": "bobsm",
"id2": "david",
"messages": [
{
"data": "Can you send the report?",
"senderId": "bobsm",
"recipientId": "david",
"timestamp": "09.10.2024 10:40:00"
},
{
"data": "I'll send it in an hour.",
"senderId": "david",
"recipientId": "bobsm",
"timestamp": "09.10.2024 10:45:00"
},
{
"data": "Perfect, thanks.",
"senderId": "bobsm",
"recipientId": "david",
"timestamp": "09.10.2024 10:50:00"
},
{
"data": "No problem.",
"senderId": "david",
"recipientId": "bobsm",
"timestamp": "09.10.2024 10:55:00"
}
]
},
{
"id1": "charl",
"id2": "evead",
"messages": [
{
"data": "Hey Eve, how's it going?",
"senderId": "charl",
"recipientId": "evead",
"timestamp": "09.10.2024 11:00:00"
},
{
"data": "Good, how about you?",
"senderId": "evead",
"recipientId": "charl",
"timestamp": "09.10.2024 11:05:00"
},
{
"data": "Can't complain!",
"senderId": "charl",
"recipientId": "evead",
"timestamp": "09.10.2024 11:10:00"
},
{
"data": "Glad to hear.",
"senderId": "evead",
"recipientId": "charl",
"timestamp": "09.10.2024 11:15:00"
}
]
},
{
"id1": "charl",
"id2": "frank",
"messages": [
{
"data": "Do you have time to talk today?",
"senderId": "charl",
"recipientId": "frank",
"timestamp": "09.10.2024 11:20:00"
},
{
"data": "I have a meeting, but I can chat afterward.",
"senderId": "frank",
"recipientId": "charl",
"timestamp": "09.10.2024 11:25:00"
},
{
"data": "Sounds good.",
"senderId": "charl",
"recipientId": "frank",
"timestamp": "09.10.2024 11:30:00"
},
{
"data": "I'll message you after.",
"senderId": "frank",
"recipientId": "charl",
"timestamp": "09.10.2024 11:35:00"
}
]
},
{
"id1": "david",
"id2": "frank",
"messages": [
{
"data": "Did you review the document?",
"senderId": "david",
"recipientId": "frank",
"timestamp": "09.10.2024 11:40:00"
},
{
"data": "Yes, it's all good.",
"senderId": "frank",
"recipientId": "david",
"timestamp": "09.10.2024 11:45:00"
},
{
"data": "Great, thanks for the quick turnaround!",
"senderId": "david",
"recipientId": "frank",
"timestamp": "09.10.2024 11:50:00"
},
{
"data": "No worries!",
"senderId": "frank",
"recipientId": "david",
"timestamp": "09.10.2024 11:55:00"
}
]
},
{
"id1": "david",
"id2": "grace",
"messages": [
{
"data": "Grace, can you send the updated schedule?",
"senderId": "david",
"recipientId": "grace",
"timestamp": "09.10.2024 12:00:00"
},
{
"data": "Yes, I'll send it in a few minutes.",
"senderId": "grace",
"recipientId": "david",
"timestamp": "09.10.2024 12:05:00"
},
{
"data": "Thanks, much appreciated!",
"senderId": "david",
"recipientId": "grace",
"timestamp": "09.10.2024 12:10:00"
},
{
"data": "You're welcome!",
"senderId": "grace",
"recipientId": "david",
"timestamp": "09.10.2024 12:15:00"
}
]
},
{
"id1": "frank",
"id2": "grace",
"messages": [
{
"data": "How are you today?",
"senderId": "frank",
"recipientId": "grace",
"timestamp": "09.10.2024 12:20:00"
},
{
"data": "I'm doing well, thanks for asking.",
"senderId": "grace",
"recipientId": "frank",
"timestamp": "09.10.2024 12:25:00"
},
{
"data": "Glad to hear that.",
"senderId": "frank",
"recipientId": "grace",
"timestamp": "09.10.2024 12:30:00"
},
{
"data": "How about you?",
"senderId": "grace",
"recipientId": "frank",
"timestamp": "09.10.2024 12:35:00"
}
]
},
{
"id1": "frank",
"id2": "hanna",
"messages": [
{
"data": "Did you attend the meeting?",
"senderId": "frank",
"recipientId": "hanna",
"timestamp": "09.10.2024 12:40:00"
},
{
"data": "Yes, it was productive.",
"senderId": "hanna",
"recipientId": "frank",
"timestamp": "09.10.2024 12:45:00"
},
{
"data": "Good to hear!",
"senderId": "frank",
"recipientId": "hanna",
"timestamp": "09.10.2024 12:50:00"
},
{
"data": "Indeed, lots to follow up on.",
"senderId": "hanna",
"recipientId": "frank",
"timestamp": "09.10.2024 12:55:00"
}
]
},
{
"id1": "grace",
"id2": "hanna",
"messages": [
{
"data": "Can we meet later today?",
"senderId": "grace",
"recipientId": "hanna",
"timestamp": "09.10.2024 01:00:00"
},
{
"data": "Sure, what's a good time?",
"senderId": "hanna",
"recipientId": "grace",
"timestamp": "09.10.2024 01:05:00"
},
{
"data": "How about 3?",
"senderId": "grace",
"recipientId": "hanna",
"timestamp": "09.10.2024 01:10:00"
},
{
"data": "Works for me.",
"senderId": "hanna",
"recipientId": "grace",
"timestamp": "09.10.2024 01:15:00"
}
]
},
{
"id1": "grace",
"id2": "ianda",
"messages": [
{
"data": "Ian, did you get the message I sent?",
"senderId": "grace",
"recipientId": "ianda",
"timestamp": "09.10.2024 01:20:00"
},
{
"data": "Yes, I'll respond soon.",
"senderId": "ianda",
"recipientId": "grace",
"timestamp": "09.10.2024 01:25:00"
},
{
"data": "Thanks, appreciate it!",
"senderId": "grace",
"recipientId": "ianda",
"timestamp": "09.10.2024 01:30:00"
},
{
"data": "You're welcome!",
"senderId": "ianda",
"recipientId": "grace",
"timestamp": "09.10.2024 01:35:00"
}
]
},
{
"id1": "hanna",
"id2": "ianda",
"messages": [
{
"data": "Ian, do you have a minute?",
"senderId": "hanna",
"recipientId": "ianda",
"timestamp": "09.10.2024 01:40:00"
},
{
"data": "Yes, what do you need?",
"senderId": "ianda",
"recipientId": "hanna",
"timestamp": "09.10.2024 01:45:00"
},
{
"data": "Just a quick update on the project.",
"senderId": "hanna",
"recipientId": "ianda",
"timestamp": "09.10.2024 01:50:00"
},
{
"data": "I'll email you the details.",
"senderId": "ianda",
"recipientId": "hanna",
"timestamp": "09.10.2024 01:55:00"
}
]
},
{
"id1": "hanna",
"id2": "jillt",
"messages": [
{
"data": "Jill, can we talk tomorrow?",
"senderId": "hanna",
"recipientId": "jillt",
"timestamp": "09.10.2024 02:00:00"
},
{
"data": "Yes, I'm free after 2.",
"senderId": "jillt",
"recipientId": "hanna",
"timestamp": "09.10.2024 02:05:00"
},
{
"data": "Perfect, see you then.",
"senderId": "hanna",
"recipientId": "jillt",
"timestamp": "09.10.2024 02:10:00"
},
{
"data": "Looking forward to it.",
"senderId": "jillt",
"recipientId": "hanna",
"timestamp": "09.10.2024 02:15:00"
}
]
},
{
"id1": "ianda",
"id2": "jillt",
"messages": [
{
"data": "Jill, I have the files you requested.",
"senderId": "ianda",
"recipientId": "jillt",
"timestamp": "09.10.2024 02:20:00"
},
{
"data": "Thanks, please send them over.",
"senderId": "jillt",
"recipientId": "ianda",
"timestamp": "09.10.2024 02:25:00"
},
{
"data": "I'll send them right now.",
"senderId": "ianda",
"recipientId": "jillt",
"timestamp": "09.10.2024 02:30:00"
},
{
"data": "Great, thanks again!",
"senderId": "jillt",
"recipientId": "ianda",
"timestamp": "09.10.2024 02:35:00"
}
]
}
]

86
stubs/api/chat/index.js Normal file
View File

@ -0,0 +1,86 @@
const chatRouter = require('express').Router();
module.exports = chatRouter;
const { getChatFromDB, getUsersChats, addChatToDB, getUserFromDB,
addMessageToChat} = require('../db');
chatRouter.get('/item/:id1/:id2', (req, res) => {
const { id1, id2 } = req.params;
if (id1 === id2) {
res.status(400).send({message: 'Ids should be different'});
return;
}
const chat = getChatFromDB(id1, id2);
if (chat) {
res.status(200).send({chat});
} else {
res.status(404).send({message: 'Chat was not found'});
}
})
chatRouter.post('/item/:id1/:id2', (req, res) => {
const { id1, id2 } = req.params;
if (id1 === id2) {
res.status(400).send({message: 'Ids should be different'});
return;
}
const chat = getChatFromDB(id1, id2);
if (chat) {
// Chat already exists
res.status(200).send({chat});
} else {
if (!getUserFromDB(id1) || !getUserFromDB(id2)) {
res.status(404).send({message: 'Such interlocutor does not exist'});
} else {
// Creating new chat
const newChat = {
id1: id1,
id2: id2,
messages: []
}
addChatToDB(newChat);
res.status(200).send({newChat});
}
}
})
chatRouter.get('/list/:id', (req, res) => {
const { id } = req.params;
const userChats = getUsersChats(id);
if (!userChats) {
res.status(404).send({message: 'Error with retrieving chats'});
} else {
res.status(200).send({chats: userChats});
}
})
chatRouter.post('/message/:sender/:receiver', (req, res) => {
const { sender, receiver } = req.params;
const { message } = req.body;
const chat = getChatFromDB(sender, receiver);
if (!chat) {
// Chat already exists
res.status(400).send({message: "Such chat does not exist"});
} else {
if (!getUserFromDB(sender) || !getUserFromDB(receiver)) {
res.status(404).send({message: 'Such people do not exist'});
} else {
// Add new message
addMessageToChat(chat, message);
res.status(200).send({});
}
}
})

74
stubs/api/db.js Normal file
View File

@ -0,0 +1,74 @@
// Read already defined users (pseudo-DB)
const users = require('./auth/users.json');
const chats = require('./chat/chats.json');
const getUserFromDB = (userID) => {
if (!userID) {return false;}
// Accessing 'DB'
const user = users.find((user) => user.id === userID);
if (user) {
return user;
} else {
return false;
}
}
const deleteUserFromDB = (userID) => {
const index = users.findIndex(item => item.id === userID);
if (index !== -1) {
users.splice(index, 1);
}
}
const addUserToDB = (user) => {
users.push(user);
}
const getChatFromDB = (firstID, secondID) => {
if (!firstID || !secondID) {return false;}
// Accessing 'DB'
const chat = chats.find((item) =>
(item.id1 === firstID && item.id2 === secondID) || (item.id1 === secondID && item.id2 === firstID));
if (chat) {
return chat;
} else {
return false;
}
}
const getUsersChats = (userID) => {
if (!userID) {return false;}
const userChats = chats.filter((chat) => (chat.id1 === userID || chat.id2 === userID));
if (userChats) {
return userChats;
} else {
return false;
}
}
const addMessageToChat = (chat, msg) => {
chat.messages.push(msg);
}
const deleteChatFromDB = (firstID, secondID) => {
const index = chats.findIndex(item =>
(item.id1 === firstID && item.id2 === secondID) || (item.id1 === secondID && item.id2 === firstID));
if (index !== -1) {
chats.splice(index, 1);
}
}
const addChatToDB = (chat) => {
chats.push(chat);
}
module.exports = {users, chats, getUserFromDB, getChatFromDB, addUserToDB,
deleteUserFromDB, addChatToDB, deleteChatFromDB, getUsersChats, addMessageToChat}

View File

@ -1,3 +1,17 @@
const changeRouter = require("./change");
const authRouter = require("./auth");
const chatRouter = require("./chat");
const router = require('express').Router();
const delay = require('./middlewares/delay');
const verify = require('./middlewares/verify');
module.exports = router;
// router.use(delay(300));
// router.use('/books', delay, booksRouter);
router.use('/auth', authRouter);
router.use('/change', verify, changeRouter);
router.use('/chat', verify, chatRouter)

3
stubs/api/key.js Normal file
View File

@ -0,0 +1,3 @@
const TOKEN_KEY = '5frv12e4few3r';
module.exports = { TOKEN_KEY }

View File

@ -0,0 +1,5 @@
const delay = (ms = 1000) => (req, res, next) => {
setTimeout(next, ms)
}
module.exports = delay

View File

@ -0,0 +1,22 @@
const jwt = require('jsonwebtoken');
const { TOKEN_KEY } = require('../key')
function verifyToken(req, res, next) {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(401).send({ message: 'No token provided' });
}
// Verify token
jwt.verify(token, TOKEN_KEY, (err, decoded) => {
if (err) {
return res.status(401).send({ message: 'Unauthorized' });
}
next(); // Proceed to the next middleware or route
});
}
module.exports = verifyToken;

View File

@ -22,4 +22,4 @@
"**/*.test.tsx",
"node_modules/@types/jest"
]
}
}

18
versions.md Normal file
View File

@ -0,0 +1,18 @@
# Instruction to create tags and versions
```
npm version <type>
```
Here <type> can be:
- `patch` - the smallest update, 1.2.3 -> 1.2.**4**
- `minor` - 1.**2**.3 -> 1.**3**.0
- `major` - the greatest, **1**.2.3 -> **2**.0.0
To submit tag (previous command is treated as a commit):
```
git push --tags
```
The version in `package.json` will be updated automatically\
After that add new version to *brojs-admin*