From 39fcf39d71943dd1b22bfc6db701896fea68dcae Mon Sep 17 00:00:00 2001 From: Stepan Pilipenko Date: Thu, 6 Nov 2025 23:23:53 +0300 Subject: [PATCH] user --- frontend/package-lock.json | 21 +++- frontend/src/app.tsx | 4 +- frontend/src/components/product-card.tsx | 34 +----- frontend/src/components/product-detail.tsx | 16 +-- frontend/src/hocs/index.ts | 1 + frontend/src/hocs/with-protection.tsx | 3 +- frontend/src/hocs/with-response.tsx | 67 ++++++++++ frontend/src/layout/header.tsx | 136 +++++---------------- frontend/src/layout/layout.tsx | 2 +- frontend/src/network.ts | 25 ++-- frontend/src/pages/login.tsx | 15 ++- frontend/src/pages/register.tsx | 42 +++++-- frontend/src/pages/user.tsx | 42 ++++--- frontend/src/storage/auth-slice.ts | 58 +++++++-- frontend/src/storage/shop-slice.ts | 2 +- frontend/src/storage/user-slice.ts | 12 +- frontend/src/utils/index.ts | 1 + frontend/src/utils/token.ts | 16 +++ 18 files changed, 279 insertions(+), 218 deletions(-) create mode 100644 frontend/src/hocs/with-response.tsx create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/src/utils/token.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f593aa4..ffe7c7e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -74,6 +74,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -389,6 +390,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -432,6 +434,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1262,6 +1265,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz", "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.4", @@ -1920,6 +1924,7 @@ "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1941,6 +1946,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2016,6 +2022,7 @@ "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2268,6 +2275,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2430,6 +2438,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2779,6 +2788,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3957,6 +3967,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -3966,6 +3977,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3978,6 +3990,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -4000,6 +4013,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -4099,7 +4113,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -4487,6 +4502,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4557,6 +4573,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4652,6 +4669,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -4745,6 +4763,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index dfc47ea..d795b5c 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -17,7 +17,7 @@ import { UserPage, } from './pages' -export const routers = createRoutesFromElements( +const routers = createRoutesFromElements( }> } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/product-card.tsx b/frontend/src/components/product-card.tsx index 9d71859..f69c711 100644 --- a/frontend/src/components/product-card.tsx +++ b/frontend/src/components/product-card.tsx @@ -1,17 +1,7 @@ -import { - Avatar, - Card, - CardActions, - CardContent, - CardHeader, - CardMedia, - Grid, - Typography, -} from '@mui/material' +import { Avatar, Card, CardContent, CardHeader, CardMedia, Grid, Typography } from '@mui/material' -import { type SyntheticEvent, useCallback } from 'react' +import { type SyntheticEvent } from 'react' import { Link } from 'react-router-dom' -import { CartButton } from './cart-button' import { useAppSelector, shop } from '../storage' export type ProductCardProps = { @@ -20,27 +10,15 @@ export type ProductCardProps = { export const ProductCard = ({ productId }: ProductCardProps) => { const product = useAppSelector(shop.selector.product(productId)) - const { id, name, count, reserved, picture_url, description, type, cost } = product || { + const { id, name, count, picture_url, description, cost } = product || { id: productId, name: '', count: 0, - reserved: 0, picture_url: '', description: '', - type: 'unknown', cost: '', } - console.debug({reserved, type}) - - //const dispatch = useAppDispatch() - - const quantity = 5 - - const handleCart = useCallback((num: number) => { - console.debug({num}) - }, []) - return ( @@ -76,12 +54,6 @@ export const ProductCard = ({ productId }: ProductCardProps) => { {`Доступно: ${count} шт.`} - - - ) diff --git a/frontend/src/components/product-detail.tsx b/frontend/src/components/product-detail.tsx index 0c19fde..a2ed9c3 100644 --- a/frontend/src/components/product-detail.tsx +++ b/frontend/src/components/product-detail.tsx @@ -11,30 +11,26 @@ import { import { useCallback } from 'react' import { CartButton } from './cart-button' import { useAppSelector, shop } from '../storage' +import { withProtection } from '../hocs' export type ProductDetailProps = { productId: number } -export const ProductDetail = ({ productId }: ProductDetailProps) => { +export const ProductDetail = withProtection(({ productId }: ProductDetailProps) => { const product = useAppSelector(shop.selector.product(productId)) - const { id, name, count, reserved, picture_url, description, type, cost } = product || { - id: productId, + const { name, count, picture_url, description, cost } = product || { name: '', count: 0, - reserved: 0, picture_url: '', description: '', - type: 'unknown', cost: '', } - console.debug({id, reserved, type}) - const quantity = 5 const handleCart = useCallback((num: number) => { - console.debug({num}) + console.debug({ num }) }, []) return ( @@ -57,7 +53,7 @@ export const ProductDetail = ({ productId }: ProductDetailProps) => { } title={name} - subheader={'subheader'} + subheader={'2025 год'} /> { ) -} +}) diff --git a/frontend/src/hocs/index.ts b/frontend/src/hocs/index.ts index 9f4d16b..0ce9510 100644 --- a/frontend/src/hocs/index.ts +++ b/frontend/src/hocs/index.ts @@ -1 +1,2 @@ export * from './with-protection' +export * from './with-response' diff --git a/frontend/src/hocs/with-protection.tsx b/frontend/src/hocs/with-protection.tsx index 12d4f66..ade9571 100644 --- a/frontend/src/hocs/with-protection.tsx +++ b/frontend/src/hocs/with-protection.tsx @@ -1,10 +1,11 @@ import { type ComponentType, type FC } from 'react' import { Navigate, useLocation } from 'react-router-dom' +import { auth, useAppSelector } from '../storage' export const withProtection =

(WrappedComponent: ComponentType

) => { const ReturnedComponent: FC

= props => { // Достаем accessToken из redux'a - const accessToken = '23123123' //useAppSelector(authSelector.accessTokenSelector) + const accessToken = useAppSelector(auth.selector.accessToken) // Объект location на понадобиться для задания состояния при redirect'e const location = useLocation() diff --git a/frontend/src/hocs/with-response.tsx b/frontend/src/hocs/with-response.tsx new file mode 100644 index 0000000..53dc280 --- /dev/null +++ b/frontend/src/hocs/with-response.tsx @@ -0,0 +1,67 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useEffect, type ComponentType, type FC } from 'react' +import { Navigate, useLocation } from 'react-router-dom' +import { auth, useAppDispatch, useAppSelector } from '../storage' +import { toast } from 'react-toastify' +import { Box } from '@mui/material' +import { Spinner } from '../components' + +type StorageType = { + selector: { + loading: any + error: any + } +} + +export const withResponse =

( + WrappedComponent: ComponentType

, + storage: T, + fecthFunc: () => any +) => { + const ReturnedComponent: FC

= props => { + const accessToken = useAppSelector(auth.selector.accessToken) + const loading: boolean = useAppSelector(storage.selector.loading) + const error: string = useAppSelector(storage.selector.error) + const dispatch = useAppDispatch() + + const location = useLocation() + + useEffect(() => { + if (accessToken) { + dispatch(fecthFunc()) + } + }, [accessToken]) + + // Если ошибка, то нужно отправить пользователя на странице входа в систему + if (error) { + toast.error(error || 'Не известная ошибка при аутентификации пользователя') + return ( + + ) + } + + if (loading) { + return ( + + + + ) + } + + return + } + + // У каждого компонента должно быть имя. Это поможет нам, когда будем использовать + // dev tools'ы реакта + ReturnedComponent.displayName = `withResponse${WrappedComponent.displayName}` + + return ReturnedComponent +} diff --git a/frontend/src/layout/header.tsx b/frontend/src/layout/header.tsx index 04e40e6..ff6b9b5 100644 --- a/frontend/src/layout/header.tsx +++ b/frontend/src/layout/header.tsx @@ -12,38 +12,24 @@ import MenuItem from '@mui/material/MenuItem' import PagesIcon from '@mui/icons-material/Pages' import { type JSX, type MouseEvent, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' -import { Badge } from '@mui/material' +import { useAppSelector, user } from '../storage' -const EXIT_NAME = 'Выйти' -const CART_NAME = 'Корзина' - -export const HEADER_TESTID = 'HEADER_TESTID' - -export const pages = [ +const pages = [ { name: 'Продукты', to: '/' }, - { name: CART_NAME, to: '/basket' }, -] -export const settings = [ - { name: 'Профиль', to: '/user' }, - { name: EXIT_NAME, to: '/' }, + { name: 'Корзина', to: '/basket' }, ] export const Header = (): JSX.Element => { const [anchorElNav, setAnchorElNav] = useState(null) - const [anchorElUser, setAnchorElUser] = useState(null) - // TODO: брать из storage - const quantitiesSum = 3 - const currentUser = { - name: 'Петя', - avatarPath: '', - } + const userName = useAppSelector(user.selector.user)?.nickname || 'Профиль' + const userAvatar = '' const handleOpenNavMenu = (event: MouseEvent) => { setAnchorElNav(event.currentTarget) } - const handleOpenUserMenu = (event: MouseEvent) => { - setAnchorElUser(event.currentTarget) + const handleOpenUser = () => { + navigate('/user') } const handleCloseNavMenu = () => { @@ -52,15 +38,6 @@ export const Header = (): JSX.Element => { const navigate = useNavigate() - const handleCloseUserMenu = - (name = '') => - () => { - if (name === EXIT_NAME) { - navigate('/logout') - } - setAnchorElUser(null) - } - return ( @@ -78,7 +55,6 @@ export const Header = (): JSX.Element => {

{ > {pages.map(page => ( - - - {page.name} - - + {page.name} + ))} {pages.map(page => ( - - - {page.name} - - + {page.name} + ))} - - - + + + - - {settings.map(setting => ( - - - {setting.name} - - - ))} - diff --git a/frontend/src/layout/layout.tsx b/frontend/src/layout/layout.tsx index 7f04d62..ecd95cb 100644 --- a/frontend/src/layout/layout.tsx +++ b/frontend/src/layout/layout.tsx @@ -14,7 +14,7 @@ export const Layout = (): JSX.Element => { useEffect(() => { dispatch(shop.fetch.products()) - }, []) + }, [dispatch]) return ( diff --git a/frontend/src/network.ts b/frontend/src/network.ts index c4bed3f..b28300e 100644 --- a/frontend/src/network.ts +++ b/frontend/src/network.ts @@ -1,3 +1,5 @@ +import { token } from './utils' + const DEV_URL = '/dev_api' const PROD_URL = 'https://shop.softwarrior.ru:8091' @@ -15,15 +17,12 @@ const DEFAULT_HEADERS: HeaderType = { } export interface NetworkApi { - get token(): string - set token(value: string) request: (endpoint: string, options?: RequestInit) => Promise } class Network implements NetworkApi { private _baseUrl: string private _headers: HeaderType - private _token: string constructor() { if (import.meta.env.DEV) { @@ -32,31 +31,25 @@ class Network implements NetworkApi { this._baseUrl = PROD_URL } this._headers = DEFAULT_HEADERS - this._token = '' } private getStringURL(endpoint: string): string { return `${this._baseUrl}/${endpoint.replace(/^\//, '')}` } - get token(): string { - return this._token + private async onResponse(res: Response): Promise { + return res.ok ? res.json() : res.json().then(data => Promise.reject(data)) } - set token(value: string) { - if (value) { - this._headers.authorization = `Bearer ${value}` + async request(endpoint: string, options?: RequestInit) { + const tokenStr = token.load() + + if (tokenStr) { + this._headers.authorization = `Bearer ${tokenStr}` } else { this._headers.authorization = undefined } - this._token = value - } - private async onResponse(res: Response): Promise { - return res.ok ? res.json() : res.json().then(data => Promise.reject(data)) - } - - async request(endpoint: string, options?: RequestInit) { const res = await fetch(this.getStringURL(endpoint), { method: 'GET', ...options, diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx index cd079f2..822b99b 100644 --- a/frontend/src/pages/login.tsx +++ b/frontend/src/pages/login.tsx @@ -4,7 +4,7 @@ import KeyIcon from '@mui/icons-material/Key' import { Controller, type SubmitHandler, useForm } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { toast } from 'react-toastify' -import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom' +import { Link as RouterLink, useNavigate } from 'react-router-dom' import * as yup from 'yup' import { auth, useAppDispatch, useAppSelector } from '../storage' import type { ErrorType } from '../types' @@ -14,29 +14,28 @@ export interface LoginFormValues { password: string } -export const loginFormSchema = yup.object({ +const loginFormSchema = yup.object({ email: yup.string().email().required(), password: yup.string().min(6).max(24).required(), }) export const LoginPage = (): JSX.Element => { const dispatch = useAppDispatch() + const token = useAppSelector(auth.selector.accessToken) const error = useAppSelector(auth.selector.error) - const { state } = useLocation() const navigate = useNavigate() useEffect(() => { if (token && !error) { toast.success('Вы успешно вошли в систему!') - // переходит туда откуда выпали на логин - navigate(state.from) + navigate('/') } if (error) { toast.error(error) } - }, [token, error]) + }, [token, error, navigate]) // инициализируем react-hook-form const { @@ -47,8 +46,8 @@ export const LoginPage = (): JSX.Element => { // с помощью generic подсказываем react-hook-form, какие поля содержит наша форма } = useForm({ defaultValues: { - email: '', - password: '', + email: 'stepan.pilipenko@xmail.ru', + password: 'password_123', }, // react-hook-form умеет работать со многими библиотеками // валидации, мы используем yup diff --git a/frontend/src/pages/register.tsx b/frontend/src/pages/register.tsx index 781a1d3..9f606b5 100644 --- a/frontend/src/pages/register.tsx +++ b/frontend/src/pages/register.tsx @@ -1,12 +1,13 @@ -import type { JSX } from 'react' +import { useEffect, type JSX } from 'react' import { Avatar, Box, Container, TextField, Typography, Button, Link } from '@mui/material' -import KeyIcon from '@mui/icons-material/Key' +import LockOutlinedIcon from '@mui/icons-material/LockOutlined' import { Controller, type SubmitHandler, useForm } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { toast } from 'react-toastify' import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom' import * as yup from 'yup' import type { ErrorType } from '../types' +import { auth, useAppDispatch, useAppSelector } from '../storage' export interface RegistrFormValues { email: string @@ -14,16 +15,33 @@ export interface RegistrFormValues { password: string } -export const registrFormSchema = yup.object({ +const registrFormSchema = yup.object({ email: yup.string().email().required(), nickname: yup.string().required().strict(), password: yup.string().min(6).max(24).required(), }) export const RegisterPage = (): JSX.Element => { + const dispatch = useAppDispatch() + const token = useAppSelector(auth.selector.accessToken) + const error = useAppSelector(auth.selector.error) + const { state } = useLocation() const navigate = useNavigate() + useEffect(() => { + if (token && !error) { + toast.success('Вы успешно зарегистрировались!') + // переходит туда откуда выпали на логин + if (state?.from) { + navigate(state?.from) + } + } + if (error) { + toast.error(error) + } + }, [token, error, navigate, state]) + // инициализируем react-hook-form const { // control понадобиться, чтобы подружить react-hook-form и компоненты из MUI @@ -44,11 +62,13 @@ export const RegisterPage = (): JSX.Element => { const submitHandler: SubmitHandler = async values => { try { - console.debug({values}) - // TODO: логинемся и запрашиваем все данные - toast.success('Вы успешно вошли в систему!') - // переходит туда откуда выпали на логин - navigate(state.from) + dispatch( + auth.fetch.register({ + email: values.email, + nickname: values.nickname, + password: values.password, + }) + ) } catch (error: unknown) { // Если произошла ошибка, то выводим уведомление const errorText = (error as ErrorType).error @@ -67,10 +87,10 @@ export const RegisterPage = (): JSX.Element => { }} > - + - Авторизация + Регистрация { fullWidth sx={{ mt: 3, mb: 2 }} > - Авторизация + Регистрация Логин diff --git a/frontend/src/pages/user.tsx b/frontend/src/pages/user.tsx index 8a3cc2b..40db234 100644 --- a/frontend/src/pages/user.tsx +++ b/frontend/src/pages/user.tsx @@ -1,35 +1,39 @@ import { Avatar, Button, Grid, Stack } from '@mui/material' import { Container } from '@mui/system' -import { withProtection } from '../hocs' +import { withProtection, withResponse } from '../hocs' import { BackButton } from '../components' +import { auth, useAppDispatch, useAppSelector, user } from '../storage' +import { useCallback } from 'react' + +const UserWithProtection = withProtection(() => { + const userData = useAppSelector(user.selector.user) + const userAvatar = '' + + const dispatch = useAppDispatch() + + const handleClick = useCallback(() => { + dispatch(auth.fetch.logout()) + }, []) -export const UserPage = withProtection(() => { - const currentUser = { - name: 'Петя', - avatarPath: '', - about: '', - } return ( -

{currentUser?.name}

-

{currentUser?.about}

+

{userData?.nickname}

+

{userData?.email}

- - +
@@ -37,3 +41,5 @@ export const UserPage = withProtection(() => {
) }) + +export const UserPage = withResponse(UserWithProtection, user, user.fetch.user) diff --git a/frontend/src/storage/auth-slice.ts b/frontend/src/storage/auth-slice.ts index 274bf5d..82ef372 100644 --- a/frontend/src/storage/auth-slice.ts +++ b/frontend/src/storage/auth-slice.ts @@ -1,7 +1,8 @@ import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' import { createAppAsyncThunk } from './hooks' -import { networkApi, type ResponseData } from '../network' +import { type ResponseData } from '../network' +import { token } from '../utils' interface AuthState { accessToken: string @@ -10,7 +11,7 @@ interface AuthState { } const initialState: AuthState = { - accessToken: '', + accessToken: token.load(), loading: false, error: undefined, } @@ -21,8 +22,10 @@ const authSlice = createSlice({ reducers: { setAccessToken(state, action: PayloadAction) { state.accessToken = action.payload.accessToken + token.save(state.accessToken) }, clearToken() { + token.save('') return initialState }, }, @@ -33,10 +36,10 @@ const authSlice = createSlice({ state.error = undefined }) .addCase(fetchLogin.fulfilled, (state, action) => { - state.accessToken = String(action.payload.success?.[0]) || '' + state.accessToken = String(action.payload.success) || '' state.loading = false state.error = undefined - networkApi.token = state.accessToken + token.save(state.accessToken) }) .addCase(fetchLogin.rejected, (state, action) => { state.loading = false @@ -54,7 +57,7 @@ const authSlice = createSlice({ state.accessToken = '' state.loading = false state.error = undefined - networkApi.token = '' + token.save('') }) .addCase(fetchLogout.rejected, (state, action) => { state.loading = false @@ -64,6 +67,24 @@ const authSlice = createSlice({ state.error = action.error.message } }) + .addCase(fetchRegister.pending, state => { + state.loading = true + state.error = undefined + }) + .addCase(fetchRegister.fulfilled, (state, action) => { + state.accessToken = String(action.payload.success) || '' + state.loading = false + state.error = undefined + token.save(state.accessToken) + }) + .addCase(fetchRegister.rejected, (state, action) => { + state.loading = false + if (action.payload) { + state.error = (action.payload as ResponseData).error + } else { + state.error = action.error.message + } + }) }, selectors: { accessToken: (state: AuthState) => state.accessToken, @@ -72,12 +93,33 @@ const authSlice = createSlice({ }, }) +type RegisterProps = { + email: string + nickname: string + password: string +} + +const fetchRegister = createAppAsyncThunk( + `${authSlice.name}/fetchRegister`, + async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => { + try { + const data = await networkApi.request('/register/', { + method: 'POST', + body: JSON.stringify(props), + }) + return fulfillWithValue(data) + } catch (error) { + return rejectWithValue(error) + } + } +) + type LoginProps = { email: string password: string } -export const fetchLogin = createAppAsyncThunk( +const fetchLogin = createAppAsyncThunk( `${authSlice.name}/fetchLogin`, async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => { try { @@ -92,7 +134,7 @@ export const fetchLogin = createAppAsyncThunk( } ) -export const fetchLogout = createAppAsyncThunk( +const fetchLogout = createAppAsyncThunk( `${authSlice.name}/fetchLogout`, async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => { try { @@ -109,7 +151,7 @@ class Auth { action = authSlice.actions reducer = authSlice.reducer name = authSlice.name - fetch = { login: fetchLogin, logout: fetchLogout } + fetch = { login: fetchLogin, logout: fetchLogout, register: fetchRegister } } export const auth = new Auth() diff --git a/frontend/src/storage/shop-slice.ts b/frontend/src/storage/shop-slice.ts index 7a7e915..0fa1e43 100644 --- a/frontend/src/storage/shop-slice.ts +++ b/frontend/src/storage/shop-slice.ts @@ -64,7 +64,7 @@ const selectProduct = (productId: number) => return products.find(product => product.id === productId) }) -export const fetchProducts = createAppAsyncThunk( +const fetchProducts = createAppAsyncThunk( `${shopSlice.name}/fetchProducts`, async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => { try { diff --git a/frontend/src/storage/user-slice.ts b/frontend/src/storage/user-slice.ts index 8f5ad93..28a4727 100644 --- a/frontend/src/storage/user-slice.ts +++ b/frontend/src/storage/user-slice.ts @@ -15,10 +15,8 @@ const initialState: UserState = { error: undefined, } -export const userSliceName = 'user' - const userSlice = createSlice({ - name: userSliceName, + name: 'user', initialState, reducers: { setUser: (state, action: PayloadAction) => { @@ -54,11 +52,13 @@ const userSlice = createSlice({ }, }) -export const fetchUser = createAppAsyncThunk( - `${userSliceName}/fetchUser`, +const fetchUser = createAppAsyncThunk( + `${userSlice.name}/fetchUser`, async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => { try { - const data = await networkApi.request('/user/') + const data = await networkApi.request('/user/', { + method: 'POST', + }) return fulfillWithValue(data) } catch (error) { return rejectWithValue(error) diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..96cac21 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1 @@ +export * from './token' diff --git a/frontend/src/utils/token.ts b/frontend/src/utils/token.ts new file mode 100644 index 0000000..a834a08 --- /dev/null +++ b/frontend/src/utils/token.ts @@ -0,0 +1,16 @@ +const TOKEN_KEY = 'accessToken' + +class Token { + save = (token: string) => { + if (token) { + localStorage.setItem(TOKEN_KEY, token) + } else { + localStorage.removeItem(TOKEN_KEY) + } + } + load = (): string => { + return localStorage.getItem(TOKEN_KEY) || '' + } +} + +export const token = new Token()