Commit 6ab37d66 authored by bekzat kapan's avatar bekzat kapan

Merge branch '7-frontend-logic' into 'master'

Resolve "Фронтенд: Бизнес-логика"

Closes #7

See merge request !13
parents 3f46dd44 1d803225
{
"root": true,
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jsx-a11y/recommended",
"prettier"
],
"plugins": ["react", "react-hooks", "@typescript-eslint", "jsx-a11y", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"rules": {
"prettier/prettier": ["error"],
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }]
},
"settings": {
"react": {
"version": "detect"
}
}
}
\ No newline at end of file
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 80
}
\ No newline at end of file
This diff is collapsed.
...@@ -3,25 +3,32 @@ ...@@ -3,25 +3,32 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev -p 0666",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.2.1", "@mui/icons-material": "^6.2.1",
"@mui/material": "^6.2.1", "@mui/material": "^6.2.1",
"@reduxjs/toolkit": "^2.5.0",
"axios": "^1.7.9",
"next": "15.1.1", "next": "15.1.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"react-redux": "^9.2.0",
"redux": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/react-redux": "^7.1.34",
"@types/redux": "^3.6.31",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.1", "eslint-config-next": "15.1.1",
"typescript": "^5" "typescript": "^5"
......
.loader {
width: 100%;
display: flex;
justify-content: center;
}
.lds-ripple {
color: #c7cad1
}
.lds-ripple,
.lds-ripple div {
box-sizing: border-box;
}
.lds-ripple {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ripple div {
position: absolute;
border: 4px solid currentColor;
opacity: 1;
border-radius: 50%;
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.lds-ripple div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes lds-ripple {
0% {
top: 36px;
left: 36px;
width: 8px;
height: 8px;
opacity: 0;
}
4.9% {
top: 36px;
left: 36px;
width: 8px;
height: 8px;
opacity: 0;
}
5% {
top: 36px;
left: 36px;
width: 8px;
height: 8px;
opacity: 1;
}
100% {
top: 0;
left: 0;
width: 80px;
height: 80px;
opacity: 0;
}
}
\ No newline at end of file
"use client";
import { store } from "@/store/store";
import { Provider } from "react-redux";
export default function RootLayout({ export default function RootLayout({
children, children,
...@@ -5,10 +8,10 @@ export default function RootLayout({ ...@@ -5,10 +8,10 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<Provider store={store}>
<html lang="en"> <html lang="en">
<body> <body>{children}</body>
{children}
</body>
</html> </html>
</Provider>
); );
} }
"use client"; 'use client';
import { Box, Button, Container, Grid2, Typography } from "@mui/material"; import { Box, Button, Container, Grid2, Typography } from '@mui/material';
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import { ChangeEvent, FormEvent, useState } from "react"; import { ChangeEvent, useEffect, useState } from 'react';
import InputField from "./components/InputField"; import InputField from './components/InputField';
import '@/animations/Loader.css';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { encodeMessage, decodeMessage } from '@/features/requestSlice';
interface IFormData { interface IFormData {
decoded: string; decoded: string;
...@@ -11,16 +14,53 @@ interface IFormData { ...@@ -11,16 +14,53 @@ interface IFormData {
} }
export default function Home() { export default function Home() {
const dispatch = useAppDispatch();
const { loading, result, error } = useAppSelector((state) => state.request);
const [formData, setFormData] = useState<IFormData>({ const [formData, setFormData] = useState<IFormData>({
encoded: "", encoded: '',
decoded: "", decoded: '',
password: "", password: '',
}); });
const submitFormHandler = (e: FormEvent<HTMLFormElement>) => { const onCypherHandler = (): void => {
e.preventDefault(); if (!formData.password.trim()) {
alert('Please, type in password!');
return;
}
if (!formData.encoded.trim()) {
alert('Please fill in the "Decoded" field!');
return;
}
dispatch(
encodeMessage({ password: formData.password, message: formData.decoded })
);
}; };
const onDecypherHandler = (): void => {
if (!formData.password.trim()) {
alert('Please, type in password!');
return;
}
if (!formData.decoded.trim()) {
alert('Please fill in the "Decoded" field!');
return;
}
dispatch(
decodeMessage({ password: formData.password, message: formData.decoded })
);
};
useEffect(() => {
if (result) {
if (formData.decoded) {
setFormData((prev) => ({ ...prev, encoded: result }));
} else {
setFormData((prev) => ({ ...prev, decoded: result }));
}
}
}, [result]);
const onInputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => { const onInputChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
setFormData((prevData) => ({ setFormData((prevData) => ({
...@@ -30,14 +70,15 @@ export default function Home() { ...@@ -30,14 +70,15 @@ export default function Home() {
}; };
return ( return (
<div className="App"> <div className="App">
<Container sx={{ marginTop: 2 }} maxWidth="lg"> <Container sx={{ marginTop: 2 }} maxWidth="lg">
<Typography <Typography
variant="h3" variant="h3"
sx={{ sx={{
fontSize: { fontSize: {
xs: "24px", xs: '24px',
sm: "36px", sm: '36px',
}, },
}} }}
> >
...@@ -46,37 +87,49 @@ export default function Home() { ...@@ -46,37 +87,49 @@ export default function Home() {
<Box <Box
sx={{ sx={{
maxWidth: { maxWidth: {
xs: "90%", xs: '90%',
sm: "80%", sm: '80%',
lg: "70%", lg: '70%',
}, },
}} }}
component="form" component="form"
autoComplete="off" autoComplete="off"
onSubmit={submitFormHandler} padding={2}
paddingY={2}
> >
<Grid2 container direction="column" spacing={2}> <Grid2 container direction="column" spacing={2}>
<InputField name="Decoded" value={formData.decoded} onChange={onInputChangeHandler} /> <InputField
name="Decoded"
value={formData.decoded}
onChange={onInputChangeHandler}
/>
<Grid2 <Grid2
container container
alignItems="center" alignItems="center"
sx={{ sx={{
flexDirection: { flexDirection: {
xs: "column", xs: 'column',
md: "row", md: 'row',
}, },
}} }}
> >
<Grid2 size={7}> <Grid2 size={7}>
<InputField name="Password" value={formData.password} onChange={onInputChangeHandler} /> <InputField
name="Password"
value={formData.password}
onChange={onInputChangeHandler}
/>
</Grid2> </Grid2>
<Grid2 container justifyContent={{}}> <Grid2 container>
<Grid2> <Grid2>
<Button size="small" variant="contained" startIcon={<ArrowDownwardIcon />}> <Button
size="small"
variant="contained"
startIcon={<ArrowDownwardIcon />}
onClick={onDecypherHandler}
>
<Typography <Typography
sx={{ sx={{
display: { xs: "none", md: "inline" }, display: { xs: 'none', md: 'inline' },
}} }}
> >
Encode Encode
...@@ -88,11 +141,16 @@ export default function Home() { ...@@ -88,11 +141,16 @@ export default function Home() {
<Button <Button
size="small" size="small"
variant="contained" variant="contained"
startIcon={<ArrowDownwardIcon sx={{ transform: "rotate(180deg)" }} />} startIcon={
<ArrowDownwardIcon
sx={{ transform: 'rotate(180deg)' }}
/>
}
onClick={onCypherHandler}
> >
<Typography <Typography
sx={{ sx={{
display: { xs: "none", md: "inline" }, display: { xs: 'none', md: 'inline' },
}} }}
> >
Decode Decode
...@@ -102,9 +160,23 @@ export default function Home() { ...@@ -102,9 +160,23 @@ export default function Home() {
</Grid2> </Grid2>
</Grid2> </Grid2>
<InputField name="Encoded" value={formData.encoded} onChange={onInputChangeHandler} /> <InputField
name="Encoded"
value={formData.encoded}
onChange={onInputChangeHandler}
/>
</Grid2> </Grid2>
</Box> </Box>
{loading && (
<div className="loader">
<div className="lds-ripple">
<div></div>
<div></div>
</div>
</div>
)}
{error && <Typography color="error">{error}</Typography>}
</Container> </Container>
</div> </div>
); );
......
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axiosClient from '@/helpers/axiosClient';
interface RequestState {
loading: boolean;
error?: string | null;
result: string;
}
interface IData {
password: string;
message: string;
}
const initialState: RequestState = {
loading: false,
error: null,
result: '',
};
export const encodeMessage = createAsyncThunk(
'request/encode',
async (data: IData) => {
try {
const response = await axiosClient.post('/encode', data);
return response.data.encoded;
} catch (error: any) {
throw new Error(
error.response?.data?.message || 'Error decoding message'
);
}
}
);
export const decodeMessage = createAsyncThunk(
'request/decode',
async (data: IData) => {
try {
const response = await axiosClient.post('/decode', data);
return response.data.decoded;
} catch (error: any) {
throw new Error(
error.response?.data?.message || 'Error encoding message'
);
}
}
);
const requestSlice = createSlice({
name: 'request',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(encodeMessage.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(encodeMessage.fulfilled, (state, action) => {
state.loading = false;
state.result = action.payload;
})
.addCase(encodeMessage.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Error occured";
})
.addCase(decodeMessage.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(decodeMessage.fulfilled, (state, action) => {
state.loading = false;
state.result = action.payload;
})
.addCase(decodeMessage.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Error occured";
});
},
});
export default requestSlice.reducer;
import axios from 'axios';
const axiosClient = axios.create({
baseURL: 'https://localhost/8000',
});
export default axiosClient;
import { useDispatch, useSelector } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "./store";
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
import { configureStore } from '@reduxjs/toolkit';
import requestReducer from '@/features/requestSlice';
export const store = configureStore({
reducer: {
request: requestReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment