42 Commits

Author SHA1 Message Date
79289457c9 39% but error 2024-10-19 10:57:44 +03:00
5f386c0f4e some progress 2024-10-19 10:48:21 +03:00
33c8f863a1 AccountButton done 2024-10-19 10:28:37 +03:00
Nikolai Petukhov
dd16f42995 working tests 2024-10-19 10:13:06 +03:00
f3e93bae19 coverage 2024-10-19 08:44:11 +03:00
07728cbbb0 tests 2024-10-19 08:16:51 +03:00
17697d7f77 tests 2024-10-19 07:48:30 +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
53 changed files with 6842 additions and 847 deletions

1
.env Normal file
View File

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

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules
.idea
.idea
coverage/

1
__mocks__/fileMock.js Normal file
View File

@@ -0,0 +1 @@
module.exports = 'test-file-stub';

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import AccountButtons from '../src/components/account/AccountButtons.jsx';
describe('AccountButtons Component', () => {
it('should render the Back link', () => {
render(<AccountButtons registered={false} />);
const backLinkElement = screen.getByRole('link', { name: /back/i });
expect(backLinkElement).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import ActionButton from '../src/components/account/ActionButton.jsx';
describe('ActionButton Component', () => {
it('should render with the correct title and call the action when clicked', () => {
const mockAction = jest.fn();
const title = 'Click Me';
render(<ActionButton action={mockAction} title={title} />);
const buttonElement = screen.getByText(title);
expect(buttonElement).toBeInTheDocument();
fireEvent.click(buttonElement);
expect(mockAction).toHaveBeenCalledTimes(1);
});
});

23
__tests__/Card.test.tsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Card from '../src/components/home/Card.jsx';
describe('Card Component', () => {
it('should render the Card component with the given ID', () => {
const testId = '123';
render(<Card id={testId} color={"FFA500FF"}/>);
const cardElement = screen.getByText(/123/i);
expect(cardElement).toBeInTheDocument();
});
it('should store the ID in local storage when clicked', () => {
const testId = '456';
render(<Card id={testId} color={"FFA500FF"}/>);
const cardElement = screen.getByText(/456/i);
fireEvent.click(cardElement);
expect(localStorage.setItem).toHaveBeenCalledWith('selectedId', testId);
});
});

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import HelloItem from '../src/components/account/HelloItem.jsx';
describe('HelloItem Component', () => {
it('should display a personalized greeting when nickname is provided', () => {
const nickname = 'JohnDoe';
const id = '12345';
render(<HelloItem nickname={nickname} id={id} />);
const greetingElement = screen.getByText(`Hello, ${nickname}!`);
const idElement = screen.getByText(`Your ID: ${id}`);
expect(greetingElement).toBeInTheDocument();
expect(idElement).toBeInTheDocument();
});
it('should display a default message when nickname is not provided', () => {
render(<HelloItem nickname="" id="12345" />);
const defaultMessage = screen.getByText("You don't have an account :(");
expect(defaultMessage).toBeInTheDocument();
});
});

60
__tests__/Home.test.jsx Normal file
View File

@@ -0,0 +1,60 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
import Home from "src/pages/Home";
import { useGetChatsQuery } from "src/backend/redux/api_slice";
// Mock Redux store
const mockStore = configureStore([]);
// Mock the useGetChatsQuery hook
jest.mock("src/backend/redux/api_slice", () => ({
useGetChatsQuery: jest.fn(),
}));
describe("Home Page", () => {
let store;
beforeEach(() => {
store = mockStore({});
});
test("renders Home page with loading state", () => {
useGetChatsQuery.mockReturnValue({ isLoading: true });
render(
<Provider store={store}>
<Home />
</Provider>
);
expect(screen.getByText(/loading/i)).toBeInTheDocument();
});
test("renders Home page with chat data", () => {
const chatsData = [{ id: 1, name: "Chat 1" }, { id: 2, name: "Chat 2" }];
useGetChatsQuery.mockReturnValue({ data: chatsData, isLoading: false });
render(
<Provider store={store}>
<Home />
</Provider>
);
expect(screen.getByText("Chat 1")).toBeInTheDocument();
expect(screen.getByText("Chat 2")).toBeInTheDocument();
});
test("renders error message when fetching chats fails", () => {
useGetChatsQuery.mockReturnValue({ error: true, isLoading: false });
render(
<Provider store={store}>
<Home />
</Provider>
);
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import InputField from '../src/components/reg/InputField.jsx';
describe('InputField Component', () => {
it('should render with the correct title and placeholder', () => {
const title = 'Username';
const placeholder = 'Enter your username';
render(<InputField title={title} placeholder={placeholder} value="" setValue={() => {}} />);
const titleElement = screen.getByText(title);
const inputElement = screen.getByPlaceholderText(placeholder);
expect(titleElement).toBeInTheDocument();
expect(inputElement).toBeInTheDocument();
});
it('should call setValue on input change', () => {
const mockSetValue = jest.fn();
const newValue = 'testUser';
render(<InputField title="Username" value="" setValue={mockSetValue} />);
const inputElement = screen.getByRole('textbox');
fireEvent.change(inputElement, { target: { value: newValue } });
expect(mockSetValue).toHaveBeenCalledWith(newValue);
});
it('should call submit function when Enter key is pressed', () => {
const mockSubmit = jest.fn();
render(<InputField title="Username" value="" setValue={() => {}} submit={mockSubmit} />);
const inputElement = screen.getByRole('textbox');
fireEvent.keyDown(inputElement, { key: 'Enter', code: 'Enter' });
expect(mockSubmit).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import NavButton from '../src/components/init/NavButton.jsx';
describe('NavButton Component', () => {
it('should render the NavButton with the correct text and link', () => {
const navLink = '/home';
const buttonText = 'Go Home';
render(<NavButton nav={navLink} text={buttonText} />);
const linkElement = screen.getByText(buttonText);
expect(linkElement).toBeInTheDocument();
expect(linkElement.closest('a')).toHaveAttribute('href', navLink);
});
});

23
__tests__/Search.test.tsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Search from '../src/components/home/Search.jsx';
describe('Search Component', () => {
it('should render the Search button', () => {
render(<Search search={() => {}} item="testItem" />);
const searchButton = screen.getByText(/find/i);
expect(searchButton).toBeInTheDocument();
});
it('should call the search function with the correct item when clicked', () => {
const mockSearch = jest.fn();
const item = 'testItem';
render(<Search search={mockSearch} item={item} />);
const searchButton = screen.getByText(/find/i);
fireEvent.click(searchButton);
expect(mockSearch).toHaveBeenCalledWith(item);
});
});

52
__tests__/SignIn.test.jsx Normal file
View File

@@ -0,0 +1,52 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import SignIn from "src/pages/SignIn";
import { displayMessage } from "src/backend/notifications/notifications";
import { post } from "src/backend/api";
// Mock the displayMessage and post functions
jest.mock("src/backend/notifications/notifications", () => ({
displayMessage: jest.fn(),
}));
jest.mock("src/backend/api", () => ({
post: jest.fn(),
}));
describe("SignIn Page", () => {
beforeEach(() => {
jest.clearAllMocks();
});
test("renders SignIn form", () => {
render(<SignIn />);
expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByText(/sign in/i)).toBeInTheDocument();
});
test("displays error message on failed login", async () => {
post.mockResolvedValueOnce({ ok: false, data: { message: "Invalid credentials" } });
render(<SignIn />);
fireEvent.change(screen.getByLabelText(/username/i), { target: { value: "user" } });
fireEvent.change(screen.getByLabelText(/password/i), { target: { value: "password" } });
fireEvent.click(screen.getByText(/sign in/i));
expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();
expect(displayMessage).toHaveBeenCalledWith("Invalid credentials", "error");
});
test("displays additional info message after multiple login attempts", async () => {
post.mockResolvedValueOnce({ ok: false, data: { message: "Invalid credentials" } });
render(<SignIn />);
fireEvent.change(screen.getByLabelText(/username/i), { target: { value: "user" } });
fireEvent.change(screen.getByLabelText(/password/i), { target: { value: "password" } });
// Simulate two failed login attempts
fireEvent.click(screen.getByText(/sign in/i));
fireEvent.click(screen.getByText(/sign in/i));
expect(displayMessage).toHaveBeenCalledWith("Note that you need to enter your ID name", "info");
});
});

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
describe('Hello World Test', () => {
it('should display hello world', () => {
// Render a simple component
render(<div>Hello World</div>);
// Check if "Hello World" is in the document
const helloWorldElement = screen.getByText(/hello world/i);
expect(helloWorldElement).toBeInTheDocument();
});
});

7
babel.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'],
};

View File

@@ -23,5 +23,6 @@ module.exports = {
},
config: {
"enterfront.api": "/api",
// paste stand URL to config
},
};

28
jest.config.ts Normal file
View File

@@ -0,0 +1,28 @@
import type { Config } from 'jest';
import { defaults } from 'jest-config';
const config: Config = {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: [
"src/components/**/*.{js,jsx,ts,tsx}", // Include all components
"src/pages/**/*.{js,jsx,ts,tsx}",
"!src/**/*.test.{js,jsx,ts,tsx}", // Exclude test files
"!src/**/index.{js,jsx,ts,tsx}", // Optionally exclude index files
],
coverageDirectory: "coverage",
coverageProvider: "v8",
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testEnvironment: "jsdom",
// transform: {
// '^.+\\.(ts|tsx|js|jsx)$': 'babel-jest',
// },
moduleNameMapper: {
"^@/src/(.*)$": "<rootDir>/src/$1", // Map '@/src' to the 'src' folder
"^src/(.*)$": "<rootDir>/src/$1", // Map 'src' to the 'src' folder
"\\.(svg|png|jpg|jpeg|gif)$": "<rootDir>/__mocks__/fileMock.js", // Add this line
},
moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx', 'js', 'jsx'],
};
export default config;

17
jest.setup.ts Normal file
View File

@@ -0,0 +1,17 @@
import { jest } from '@jest/globals';
import '@testing-library/jest-dom';
import mockedConfig from './bro.config.js'
jest.mock('@brojs/cli', () => {
return {
getNavigations() {
return mockedConfig.navigations
},
getNavigationsValue(key) {
return mockedConfig.navigations[key]
},
getConfigValue(key) {
return mockedConfig.config[key]
}
}
})

5336
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,22 @@
"@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",
"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"
@@ -21,8 +28,25 @@
"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"
"clean": "rimraf dist",
"test": "jest",
"test:watch": "jest --watchAll"
},
"name": "enterfront",
"version": "0.2.3"
"version": "0.5.4",
"devDependencies": {
"@babel/core": "^7.25.8",
"@babel/preset-env": "^7.25.8",
"@babel/preset-react": "^7.25.7",
"@babel/preset-typescript": "^7.25.7",
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.0.1",
"@types/jest": "^29.5.13",
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"redux-mock-store": "^1.5.4",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2"
}
}

View File

@@ -1,15 +1,42 @@
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>
)
}

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 = LOCAL + 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

@@ -1,43 +0,0 @@
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Map();
wss.on("connection", (ws, req) => {
console.log("New client connected");
ws.on("message", (message) => {
try {
const parsedMessage = JSON.parse(message);
if (parsedMessage.type === "register") {
clients.set(parsedMessage.userId, ws);
console.log(`User registered: ${parsedMessage.userId}`);
} else if (parsedMessage.type === "message") {
const recipientWs = clients.get(parsedMessage.recipientId);
if (recipientWs) {
recipientWs.send(
JSON.stringify({
senderId: parsedMessage.senderId,
message: parsedMessage.message,
timestamp: new Date().toISOString(),
})
);
} else {
console.error(`User ${parsedMessage.recipientId} is not connected.`);
}
}
} catch (err) {
console.error("Error processing message:", err.message);
}
});
ws.on("close", () => {
console.log("Client disconnected");
[...clients.entries()].forEach(([userId, clientWs]) => {
if (clientWs === ws) {
clients.delete(userId);
console.log(`User disconnected: ${userId}`);
}
});
});
});

View File

@@ -1,8 +0,0 @@
export default class Interlocutor {
constructor(id, name) {
this.name = name;
this.id = id;
}
static name;
static id;
}

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

@@ -1,7 +0,0 @@
export default class User {
constructor(id, name) {
this.id = id;
this.name = name;
this.status = "online";
}
}

View File

@@ -1,14 +1,48 @@
import React from 'react';
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">
<ActionButton title={"Exit"} action={props.exitHandler}/>
<ActionButton title={"Change Name"} action={props.changeNameHandler}/>
<ActionButton title={"Change Pass"} action={props.changePassHandler}/>
{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>
);

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

@@ -3,6 +3,8 @@
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 30px;
}
.account-buttons a {
@@ -20,10 +22,33 @@
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

@@ -1,40 +1,92 @@
import React from 'react';
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 { chats } = props;
const [customChats, setCustomChats] = useState([]);
const colorMap = {
orange: 'FFA500FF',
aqua: '00FFFFFF',
crimson: 'DC143CFF',
red: 'FF0000FF',
violet: '8A2BE2FF',
seagreen: '20B2AAFF',
green: 'ADFF2FFF',
blue: '0000FFFF',
pink: 'FF1493FF',
cyan: '72FAFAFF'
}
const updateList = async () => {
const username = localStorage.getItem("username");
if (!username) return null;
function getColor(chatId) {
const keys = Object.keys(colorMap);
const index = chatId % keys.length;
return colorMap[keys[index]];
}
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">
{chats.map((item, index) => (
<Card
key={index}
name={item.name}
lastMessage={item.lastMessage}
id={item.id}
color={getColor(item.id)}
/>
))}
</div>
<div className="ChatsList">
{customChats.map((item, index) => (
<Card
key={index}
name={item.name}
lastMessage={item.lastMessageData}
id={item.id}
color={getColor(item.id)}
/>
))}
</div>
);
};

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

@@ -12,11 +12,30 @@
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 {

View File

@@ -1,6 +1,7 @@
import React from 'react';
const InputField = (props) => {
console.log('class:', props.className)
return (
<div>
<p>{props.title}</p>
@@ -8,6 +9,14 @@ const InputField = (props) => {
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.submit();
}
}
}}
/>
</div>
);

View File

@@ -1,19 +1,78 @@
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 = () => {
const exitHandler = () => {}
const changeNameHandler = () => {}
const changePassHandler = () => {}
const exitHandler = () => {
localStorage.removeItem("username");
localStorage.removeItem("token");
localStorage.setItem("message", "Exited successfully!");
window.location.href = "/";
}
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>
);

View File

@@ -2,6 +2,10 @@ 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 = [
"😀",
@@ -74,7 +78,7 @@ const emojis = [
];
const Chat = () => {
const [interlocutorId, setInterlocutorId] = useState(0);
const [interlocutorId, setInterlocutorId] = useState("");
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
@@ -82,54 +86,82 @@ const Chat = () => {
const chatRef = useRef(null);
const navigate = useNavigate();
const [myId, setMyId] = useState("");
useEffect(() => {
const id = parseInt(localStorage.getItem("interlocutorId"), 10) || 0;
const id = localStorage.getItem("interlocutorId");
setInterlocutorId(id);
socket.current = new WebSocket("ws://localhost:8080");
const username = localStorage.getItem("username");
setMyId(username);
socket.current.onopen = () => {
console.log("WebSocket connected");
socket.current.send(
JSON.stringify({ type: "register", userId: "yourUserId" })
);
};
if (!id || !username) {
displayMessage("You are not logged in!", MessageType.WARN);
return () => {};
}
socket.current.onmessage = (event) => {
const receivedData = JSON.parse(event.data);
setMessages((prev) => [...prev, receivedData]);
};
socket.current = io("http://localhost:8099");
socket.current.onerror = (event) => {
console.error("WebSocket error observed:", event);
};
socket.current.on("receiveMessage", (message) => {
setMessages((prev) => [...prev, message]);
});
socket.current.onclose = () => {
console.log("WebSocket closed");
};
socket.current.on("connect_error", (err) => {
console.error("Connection Error:", err.message);
});
return () => {
socket.current.close();
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 = {
type: "message",
senderId: "yourUserId",
senderId: myId,
recipientId: interlocutorId,
message: newMessage,
timestamp: new Date().toLocaleTimeString(),
data: newMessage,
timestamp: new Date().toLocaleString(),
};
socket.current.send(JSON.stringify(messageData));
socket.current.emit("sendMessage", messageData);
setMessages((prev) => [...prev, messageData]);
sendMessageToDB(messageData).then();
setNewMessage("");
}
};
@@ -154,20 +186,18 @@ const Chat = () => {
className="home-button"
>
Home
</button>{" "}
{}
</button>
</div>
<div className="chat-messages" ref={chatRef}>
{messages.map((msg, index) => (
<div
key={index}
className={`message-bubble ${
msg.senderId === "yourUserId" ? "sent" : "received"
msg.senderId === myId ? "sent" : "received"
}`}
>
<div className="message-content">
<b>{msg.senderId === "yourUserId" ? "You" : "Interlocutor"}:</b>{" "}
{msg.message}
<b>{msg.senderId === myId ? "You" : "They"}:</b> {msg.data}
</div>
<span className="message-timestamp">{msg.timestamp}</span>
</div>
@@ -180,7 +210,7 @@ const Chat = () => {
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message..."
className="chat-input"
onKeyPress={handleKeyPress}
onKeyDown={handleKeyPress}
/>
<button
className="emoji-button"

View File

@@ -1,76 +1,115 @@
import React from "react";
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("");
// temp for testing
const chats = [
{
name: "Alice Johnson",
id: 123456,
lastMessage: "See you later!"
},
{
name: "Bob Smith",
id: 654321,
lastMessage: "Got it, thanks!"
},
{
name: "Charlie Brown",
id: 234567,
lastMessage: "How's the project going? How's the project going? How's the project going?" +
"How's the project going? How's the project going?"
},
{
name: "David Clark",
id: 765432,
lastMessage: "I'll send the files."
},
{
name: "Eve Adams",
id: 345678,
lastMessage: "Let's meet tomorrow."
},
{
name: "Frank Wright",
id: 876543,
lastMessage: "Can you review this?"
},
{
name: "Grace Lee",
id: 456789,
lastMessage: "Thanks for your help!"
},
{
name: "Hannah Scott",
id: 987654,
lastMessage: "See you at the meeting."
},
{
name: "Ian Davis",
id: 567890,
lastMessage: "Let me know when you're free."
},
{
name: "Jill Thompson",
id: 678901,
lastMessage: "I'll catch up with you later."
}
];
const username = localStorage.getItem("username");
return (
// 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>
<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} />
</>
)}
<HomeTitle/>
<p>Your chats</p>
<ChatsList chats={chats} />
</div>
)
}
);
};
export default Home
export default Home;

View File

@@ -3,12 +3,42 @@ 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 submit = (e) => {
console.log('Sign In!')
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 (

View File

@@ -2,20 +2,60 @@ 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("");
const submit = (e) => {
console.log('Sign Up!')
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}/>

View File

@@ -1,3 +1,7 @@
body {
background: linear-gradient(to right, #e0f7fa, #fffde7);
}
.chat-container {
display: flex;
flex-direction: column;
@@ -11,10 +15,13 @@
}
.chat-header {
padding: 10px;
padding: 15px;
background-color: #007bff;
color: white;
text-align: center;
font-weight: bold;
letter-spacing: 1px;
border-bottom: 2px solid #0056b3;
}
.chat-messages {
@@ -24,6 +31,7 @@
padding: 10px;
overflow-y: auto;
background-color: #fff;
max-height: 400px;
}
.message-bubble {
@@ -33,6 +41,12 @@
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 {
@@ -58,8 +72,9 @@
.message-timestamp {
font-size: 10px;
color: #999;
margin-top: 5px;
color: black;
text-align: right;
margin-top: 4px;
display: block;
}

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

@@ -13,7 +13,7 @@
"target": "es6",
"jsx": "react",
"typeRoots": ["node_modules/@types", "src/typings"],
"types" : ["webpack-env", "node"],
"types" : ["webpack-env", "node", "jest"],
"resolveJsonModule": true
},
"exclude": [