Теперь есть возможность регистрации и авторизации. Осталось настроить выход и автопродление токена))

This commit is contained in:
2025-09-20 23:51:02 +04:00
parent ba1ad50382
commit 51e33fd62d
10 changed files with 763 additions and 25 deletions

View File

@@ -14,7 +14,9 @@
"@testing-library/user-event": "^13.5.0",
"axios": "^1.12.2",
"bootstrap": "^5.3.8",
"jwt-decode": "^4.0.0",
"react": "^19.1.1",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2",
"react-router-hash-link": "^2.4.3",
@@ -2882,12 +2884,75 @@
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@restart/hooks": {
"version": "0.4.16",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
"dependencies": {
"dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz",
"integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==",
"dependencies": {
"@babel/runtime": "^7.26.0",
"@popperjs/core": "^2.11.8",
"@react-aria/ssr": "^3.5.0",
"@restart/hooks": "^0.5.0",
"@types/warning": "^3.0.3",
"dequal": "^2.0.3",
"dom-helpers": "^5.2.0",
"uncontrollable": "^8.0.4",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/@restart/ui/node_modules/@restart/hooks": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz",
"integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==",
"dependencies": {
"dequal": "^2.0.3"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui/node_modules/uncontrollable": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
"integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
"peerDependencies": {
"react": ">=16.14.0"
}
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3211,6 +3276,14 @@
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
@@ -3537,6 +3610,11 @@
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz",
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="
},
"node_modules/@types/q": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
@@ -3552,6 +3630,22 @@
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
},
"node_modules/@types/react": {
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.12",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -3615,6 +3709,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"node_modules/@types/warning": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q=="
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -5247,6 +5346,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="
},
"node_modules/classnames": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@@ -5944,6 +6048,11 @@
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -6239,6 +6348,15 @@
"utila": "~0.4"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@@ -8607,6 +8725,14 @@
"node": ">= 0.4"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -10246,6 +10372,14 @@
"node": ">=4.0"
}
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"engines": {
"node": ">=18"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -12625,6 +12759,23 @@
"react-is": "^16.13.1"
}
},
"node_modules/prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"dependencies": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/prop-types-extra/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -12795,6 +12946,36 @@
"node": ">=14"
}
},
"node_modules/react-bootstrap": {
"version": "2.10.10",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz",
"integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==",
"dependencies": {
"@babel/runtime": "^7.24.7",
"@restart/hooks": "^0.4.9",
"@restart/ui": "^1.9.4",
"@types/prop-types": "^15.7.12",
"@types/react-transition-group": "^4.4.6",
"classnames": "^2.3.2",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.8.1",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.5",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"@types/react": ">=16.14.8",
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@@ -12915,6 +13096,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -13051,6 +13237,21 @@
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -15251,6 +15452,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/underscore": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
@@ -15478,6 +15693,14 @@
"makeerror": "1.0.12"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",

View File

@@ -9,7 +9,9 @@
"@testing-library/user-event": "^13.5.0",
"axios": "^1.12.2",
"bootstrap": "^5.3.8",
"jwt-decode": "^4.0.0",
"react": "^19.1.1",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.2",
"react-router-hash-link": "^2.4.3",

View File

@@ -1,8 +1,34 @@
import axios from "axios";
import {
getAccessToken,
isTokenExpired,
removeAccessToken,
setAccessToken,
} from "./JwtUtils";
const ONLINE_LIBRARY_URL = "http://localhost:8080/api/v1.0";
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else {
prom.resolve(token);
}
});
failedQueue = [];
};
const ONLINE_LIBRARY_URL = "http://localhost:8080";
const api = axios.create({
baseURL: ONLINE_LIBRARY_URL + "/api/v1.0",
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
const apiAuth = axios.create({
baseURL: ONLINE_LIBRARY_URL,
timeout: 10000,
withCredentials: true,
@@ -11,6 +37,86 @@ const api = axios.create({
},
});
api.interceptors.request.use(
async (config) => {
const token = getAccessToken();
if (token) {
if (isTokenExpired(token)) {
if (!isRefreshing) {
isRefreshing = true;
try {
const response = await apiAuth.post("/reload");
const newToken = response.data;
setAccessToken(newToken);
token = newToken;
processQueue(null, newToken);
} catch (error) {
processQueue(error, null);
removeAccessToken();
return Promise.reject(error);
} finally {
isRefreshing = false;
}
} else {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
config.headers.Authorization = `Bearer ${token}`;
return config;
})
.catch((error) => Promise.reject(error));
}
}
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
if (!isRefreshing) {
isRefreshing = true;
try {
const response = await apiAuth.post("/reload");
const newToken = response.data;
setAccessToken(newToken);
processQueue(null, newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
} catch (error) {
processQueue(error, null);
removeAccessToken();
return Promise.reject(error);
} finally {
isRefreshing = false;
}
} else {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then((token) => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
})
.catch((error) => Promise.reject(error));
}
}
return Promise.reject(error);
}
);
export const bookAPI = {
getAll: (params = {}) => {
return api.get("/books", { params });
@@ -31,3 +137,15 @@ export const bookAPI = {
return api.delete("/book", { params: { id } });
},
};
export const authAPI = {
login: (user, params = {}) => {
return apiAuth.post("/login", user, { params });
},
registration: (user, params = {}) => {
return apiAuth.post("/registration", user, { params });
},
logout: (params = {}) => {
return apiAuth.get("/logout", { params });
},
};

View File

@@ -0,0 +1,18 @@
import {jwtDecode} from "jwt-decode";
export const getAccessToken = () => localStorage.getItem("access_token");
export const setAccessToken = (token) =>
localStorage.setItem("access_token", token);
export const removeAccessToken = () => localStorage.removeItem("access_token");
export const isTokenExpired = (token) => {
if (!token) return true;
try {
const decodedToken = jwtDecode(token);
const currentTime = Date.now() / 1000;
return decodedToken.exp < currentTime;
} catch (error) {
console.error("Error decoding token:", error);
return true;
}
};

View File

@@ -6,6 +6,7 @@ import { BrowserRouter, Route, Routes } from "react-router-dom";
import Lk from "./Lk/Lk";
import Choice from "./Choice/Choice";
import BookCardInfo from "./BookCardInfo/BookCardIInfo";
import AuthPages from "./Registration/AuthPages";
function App() {
return (
@@ -18,6 +19,7 @@ function App() {
<Route path="/choice" element={<Choice />}></Route>
<Route path="/lk" element={<Lk />}></Route>
<Route path="/infobook/:id" element={<BookCardInfo />}></Route>
<Route path="/registration" element={<AuthPages></AuthPages>}></Route>
</Routes>
<Footer></Footer>
</BrowserRouter>

View File

@@ -3,18 +3,26 @@ import "bootstrap/dist/js/bootstrap.bundle.min";
import "../style.css";
import BookCard from "../BookCard/BookCard";
import { useEffect, useState } from "react";
import { bookAPI } from "../ApiRequest/ApiClient";
import { authAPI, bookAPI } from "../ApiRequest/ApiClient";
import { setAccessToken } from "../ApiRequest/JwtUtils";
import { Spinner } from "react-bootstrap";
function Main() {
const [bookData, setBookData] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const testUser = {
username: "test21",
password: "123456789",
};
useEffect(() => {
const fetchBooks = async () => {
try {
setIsLoading(true);
const response = await bookAPI.getAll();
setBookData(response.data);
setBookData(response.data.content);
} catch (error) {
setError(error.message);
} finally {
@@ -23,7 +31,7 @@ function Main() {
};
fetchBooks();
}, []);
if (isLoading) return <div> Загрузка</div>;
if (isLoading) return <Spinner animation="border" size="sm" />;
if (error) return <div> Ошибка: {error}</div>;
return (
<div className="container">
@@ -32,22 +40,6 @@ function Main() {
{bookData.map((book) => (
<BookCard book={book}></BookCard>
))}
{/* <BookCard
title={"Джордж укыавяыав"}
linkOnImage={"../resources/1984.jpg"}
></BookCard>
<BookCard
title={"Джордж укыавяыав"}
linkOnImage={"../resources/1984.jpg"}
></BookCard>
<BookCard
title={"Джордж укыавяыав"}
linkOnImage={"../resources/1984.jpg"}
></BookCard>
<BookCard
title={"Джордж укыавяыав"}
linkOnImage={"../resources/1984.jpg"}
></BookCard> */}
</div>
</div>
);

View File

@@ -0,0 +1,331 @@
import React, { use, useState } from "react";
import {
Container,
Row,
Col,
Form,
Button,
Card,
Tab,
Tabs,
Alert,
} from "react-bootstrap";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap/dist/js/bootstrap.bundle.min";
import { authAPI } from "../ApiRequest/ApiClient";
import axios from "axios";
import { setAccessToken } from "../ApiRequest/JwtUtils";
import { useNavigate } from "react-router-dom";
const AuthPages = () => {
const [activeTab, setActiveTab] = useState("login");
return (
<div
style={{
backgroundColor: "var(--bg-dark)",
minHeight: "100vh",
padding: "20px 0",
}}
>
<Container>
<Row className="justify-content-center">
<Col md={6} lg={5}>
<Card
style={{
backgroundColor: "var(--bg-medium)",
border: "none",
borderRadius: "10px",
overflow: "hidden",
}}
>
<Card.Body className="p-0">
<Tabs
activeKey={activeTab}
onSelect={(k) => setActiveTab(k)}
fill
style={{
backgroundColor: "var(--bg-dark)",
}}
>
<Tab eventKey="login" title="Вход">
<LoginForm />
</Tab>
<Tab eventKey="register" title="Регистрация">
<RegisterForm />
</Tab>
</Tabs>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
</div>
);
};
const LoginForm = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const [error, setError] = useState(null);
const [formData, setFormData] = useState({
username: "",
password: "",
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
// Обработка входа
console.log("Login data:", formData);
setLoading(true);
try {
const response = await authAPI.login(formData);
if (response.status == 200) {
const token = response.data;
setAccessToken(token);
setTimeout(() => {
navigate("/");
}, 1500);
}
} catch (error) {
if (axios.isAxiosError(error)) {
setError(error.response.data.message);
} else {
setError("Ошибка сети. Повторите попытку чуть чуть попозже");
}
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: "30px" }}>
<h3 className="text-center mb-4" style={{ color: "var(--text-light)" }}>
Вход в аккаунт
</h3>
{error && <Alert variant="danger">{error}</Alert>}
{message && <Alert variant="success">{message}</Alert>}
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label style={{ color: "var(--text-light)" }}>
Email/Логин
</Form.Label>
<Form.Control
type="text"
name="username"
value={formData.username}
onChange={handleChange}
required
style={{
backgroundColor: "var(--bg-dark)",
border: "1px solid var(--accent)",
color: "var(--text-light)",
}}
/>
</Form.Group>
<Form.Group className="mb-4">
<Form.Label style={{ color: "var(--text-light)" }}>Пароль</Form.Label>
<Form.Control
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
style={{
backgroundColor: "var(--bg-dark)",
border: "1px solid var(--accent)",
color: "var(--text-light)",
}}
/>
</Form.Group>
<div className="d-grid gap-2">
<Button
type="submit"
style={{
backgroundColor: "var(--accent)",
border: "none",
fontWeight: "bold",
}}
size="lg"
>
Войти
</Button>
</div>
</Form>
</div>
);
};
const RegisterForm = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [message, setMessage] = useState("");
const [formData, setFormData] = useState({
username: "",
fio: "",
email: "",
password: "",
confirmPassword: "",
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e) => {
e.preventDefault();
console.log("Register data:", formData);
setLoading(true);
if (formData.password !== formData.confirmPassword) {
setError("Пароли не совпадают!");
setLoading(false);
return;
}
try {
const response = await authAPI.registration(formData);
if (response.data) {
setMessage("Регистрация прошла успешно!");
setFormData({
fio: "",
email: "",
username: "",
password: "",
confirmPassword: "",
});
setTimeout(() => {}, 2000);
} else {
setError(response.data.message || "Ошибка регистрации");
}
} catch (error) {
if (axios.isAxiosError(error)) {
setError(error.response.data.message);
} else {
setError("Ошибка сети. Попробуйте позже.");
}
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: "30px" }}>
<h3 className="text-center mb-4" style={{ color: "var(--text-light)" }}>
Создать аккаунт
</h3>
{error && <Alert variant="danger">{error}</Alert>}
{message && <Alert variant="success">{message}</Alert>}
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3">
<Form.Label style={{ color: "var(--text-light)" }}>Логин</Form.Label>
<Form.Control
type="text"
name="username"
value={formData.username}
onChange={handleChange}
required
style={{
backgroundColor: "var(--bg-dark)",
border: "1px solid var(--accent)",
color: "var(--text-light)",
}}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label style={{ color: "var(--text-light)" }}>Имя</Form.Label>
<Form.Control
type="text"
name="fio"
value={formData.fio}
onChange={handleChange}
required
style={{
backgroundColor: "var(--bg-dark)",
border: "1px solid var(--accent)",
color: "var(--text-light)",
}}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label style={{ color: "var(--text-light)" }}>Email</Form.Label>
<Form.Control
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
style={{
backgroundColor: "var(--bg-dark)",
border: "1px solid var(--accent)",
color: "var(--text-light)",
}}
/>
</Form.Group>
<Form.Group className="mb-3">
<Form.Label style={{ color: "var(--text-light)" }}>Пароль</Form.Label>
<Form.Control
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
style={{
backgroundColor: "var(--bg-dark)",
border: "1px solid var(--accent)",
color: "var(--text-light)",
}}
/>
</Form.Group>
<Form.Group className="mb-4">
<Form.Label style={{ color: "var(--text-light)" }}>
Подтвердите пароль
</Form.Label>
<Form.Control
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleChange}
required
style={{
backgroundColor: "var(--bg-dark)",
border: "1px solid var(--accent)",
color: "var(--text-light)",
}}
/>
</Form.Group>
<div className="d-grid gap-2">
<Button
type="submit"
style={{
backgroundColor: "var(--accent)",
border: "none",
fontWeight: "bold",
}}
size="lg"
>
Зарегистрироваться
</Button>
</div>
</Form>
</div>
);
};
export default AuthPages;

View File

@@ -2,11 +2,33 @@ import "bootstrap/dist/css/bootstrap.css";
import "bootstrap/dist/js/bootstrap.bundle.min";
import "../style.css";
import BookCard from "../BookCard/BookCard";
import { useEffect, useState } from "react";
import { bookAPI } from "../ApiRequest/ApiClient";
function Search() {
const [books, setBooks] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchBooks = async () => {
try {
setLoading(true);
const response = await bookAPI.getAll();
setBooks(response.data.content);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchBooks();
}, []);
if (loading) return <div> Загрузка</div>;
if (error) return <div> Ошибка: {error}</div>;
return (
<main className="container">
<div className="mb-5 mx-auto" style={{maxWidth:"6000px"}}>
<div className="mb-5 mx-auto" style={{ maxWidth: "6000px" }}>
<input
type="text"
className="form-control form-control-lg"
@@ -15,7 +37,10 @@ function Search() {
</div>
<div className="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
<BookCard title={"rtjdxfgvhjksfd"} linkOnImage={"/resources/1984.jpg"}></BookCard>
<BookCard
title={"rtjdxfgvhjksfd"}
linkOnImage={"/resources/1984.jpg"}
></BookCard>
</div>
</main>
);

View File

@@ -3,6 +3,7 @@ import "bootstrap/dist/js/bootstrap.bundle.min";
import "../style.css";
import { NavLink } from "react-router-dom";
import { HashLink } from "react-router-hash-link";
import { getAccessToken } from "../ApiRequest/JwtUtils";
function Headers() {
return (
@@ -70,8 +71,8 @@ function Headers() {
</ul>
</li>
<li className="nav-item">
<NavLink to="/lk" className="nav-link nav-link-custom">
Личный кабинет
<NavLink to={getAccessToken() ? "/lk" : "/registration"} className="nav-link nav-link-custom">
{getAccessToken() ? "Личный кабинет" : "Вход/Регистрация"}
</NavLink>
</li>
</ul>

View File

@@ -80,4 +80,30 @@ body {
.book-item:hover {
transform: translateX(10px);
}
.nav-tabs .nav-link {
color: var(--text-light);
background-color: transparent;
border: none;
}
.nav-tabs .nav-link.active {
color: var(--accent);
background-color: var(--bg-medium);
border: none;
font-weight: bold;
}
.nav-tabs {
border-bottom: 1px solid var(--accent);
}