main
Stepan Pilipenko 1 month ago
parent 84da23519c
commit 39fcf39d71

@ -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"
},

@ -17,7 +17,7 @@ import {
UserPage,
} from './pages'
export const routers = createRoutesFromElements(
const routers = createRoutesFromElements(
<Route path="/" id="root" element={<Layout />}>
<Route index element={<ShopPage />} />
<Route
@ -27,7 +27,7 @@ export const routers = createRoutesFromElements(
errorElement={<NotFoundPage />}
/>
<Route path="/user" element={<UserPage />} />
<Route path="/registr" element={<RegisterPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/logout" element={<LogoutPage />} />
<Route path="*" element={<NotFoundPage />} />

@ -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 (
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 4 }}>
<Card elevation={3}>
@ -76,12 +54,6 @@ export const ProductCard = ({ productId }: ProductCardProps) => {
{`Доступно: ${count} шт.`}
</Typography>
</CardContent>
<CardActions
sx={{ mt: 'auto', display: 'flex', justifyContent: 'space-between' }}
disableSpacing
>
<CartButton count={quantity} onClick={handleCart} />
</CardActions>
</Card>
</Grid>
)

@ -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) => {
</Avatar>
}
title={name}
subheader={'subheader'}
subheader={'2025 год'}
/>
<CardMedia
component="img"
@ -86,4 +82,4 @@ export const ProductDetail = ({ productId }: ProductDetailProps) => {
</Card>
</Grid>
)
}
})

@ -1 +1,2 @@
export * from './with-protection'
export * from './with-response'

@ -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 = <P extends object>(WrappedComponent: ComponentType<P>) => {
const ReturnedComponent: FC<P> = props => {
// Достаем accessToken из redux'a
const accessToken = '23123123' //useAppSelector(authSelector.accessTokenSelector)
const accessToken = useAppSelector(auth.selector.accessToken)
// Объект location на понадобиться для задания состояния при redirect'e
const location = useLocation()

@ -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 = <P extends object, T extends StorageType>(
WrappedComponent: ComponentType<P>,
storage: T,
fecthFunc: () => any
) => {
const ReturnedComponent: FC<P> = 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 (
<Navigate
to="/login"
// при этом мы передаем состояние, в котором указываем, какую
// страницу хотел посетить пользователь. И если он в дальнейшем
// войдет в систему, то мы его автоматически перебросим на желаемую страницу
state={{
from: location.pathname,
}}
/>
)
}
if (loading) {
return (
<Box display="flex" justifyContent="center" alignItems="center" minHeight="100vh">
<Spinner />
</Box>
)
}
return <WrappedComponent {...props} />
}
// У каждого компонента должно быть имя. Это поможет нам, когда будем использовать
// dev tools'ы реакта
ReturnedComponent.displayName = `withResponse${WrappedComponent.displayName}`
return ReturnedComponent
}

@ -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 | HTMLElement>(null)
const [anchorElUser, setAnchorElUser] = useState<null | HTMLElement>(null)
// TODO: брать из storage
const quantitiesSum = 3
const currentUser = {
name: 'Петя',
avatarPath: '',
}
const userName = useAppSelector(user.selector.user)?.nickname || 'Профиль'
const userAvatar = ''
const handleOpenNavMenu = (event: MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget)
}
const handleOpenUserMenu = (event: MouseEvent<HTMLElement>) => {
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 (
<AppBar position="sticky">
<Container maxWidth="lg">
@ -78,7 +55,6 @@ export const Header = (): JSX.Element => {
<MenuIcon />
</IconButton>
<Menu
data-testid={HEADER_TESTID}
id="menu-appbar"
anchorEl={anchorElNav}
anchorOrigin={{
@ -98,93 +74,45 @@ export const Header = (): JSX.Element => {
>
{pages.map(page => (
<MenuItem key={page.name} onClick={handleCloseNavMenu}>
<Badge
badgeContent={
page.name === CART_NAME ? quantitiesSum : null
}
color="warning"
sx={{ mr: 1 }}
<Typography
component={Link}
to={page.to}
sx={{
color: 'inherit',
textDecoration: 'none',
}}
textAlign="center"
>
<Typography
component={Link}
to={page.to}
sx={{
color: 'inherit',
textDecoration: 'none',
}}
textAlign="center"
>
{page.name}
</Typography>
</Badge>
{page.name}
</Typography>
</MenuItem>
))}
</Menu>
</Box>
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
{pages.map(page => (
<Badge
<Typography
key={page.name}
badgeContent={page.name === CART_NAME ? quantitiesSum : null}
color="warning"
sx={{ mr: 1 }}
onClick={handleCloseNavMenu}
sx={{
mr: 2,
color: 'white',
display: 'block',
textDecoration: 'none',
}}
component={Link}
to={page.to}
>
<Typography
key={page.name}
onClick={handleCloseNavMenu}
sx={{
mr: 2,
color: 'white',
display: 'block',
textDecoration: 'none',
}}
component={Link}
to={page.to}
>
{page.name}
</Typography>
</Badge>
{page.name}
</Typography>
))}
</Box>
<Box sx={{ flexGrow: 0 }}>
<Tooltip title="Open settings">
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
<Avatar alt={currentUser?.name} src={currentUser?.avatarPath} />
<Tooltip title={userName}>
<IconButton onClick={handleOpenUser} sx={{ p: 0 }}>
<Avatar alt={userName} src={userAvatar} />
</IconButton>
</Tooltip>
<Menu
sx={{ mt: '45px' }}
id="menu-appbar"
anchorEl={anchorElUser}
anchorOrigin={{
vertical: 'top',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorElUser)}
onClose={handleCloseUserMenu()}
>
{settings.map(setting => (
<MenuItem
key={setting.name}
onClick={handleCloseUserMenu(setting.name)}
>
<Typography
sx={{ textDecoration: 'none' }}
component={Link}
color="inherit"
to={setting.to}
textAlign="center"
>
{setting.name}
</Typography>
</MenuItem>
))}
</Menu>
</Box>
</Toolbar>
</Container>

@ -14,7 +14,7 @@ export const Layout = (): JSX.Element => {
useEffect(() => {
dispatch(shop.fetch.products())
}, [])
}, [dispatch])
return (
<ThemeProvider theme={theme}>

@ -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: <T>(endpoint: string, options?: RequestInit) => Promise<T>
}
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<T>(res: Response): Promise<T> {
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<T>(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<T>(res: Response): Promise<T> {
return res.ok ? res.json() : res.json().then(data => Promise.reject(data))
}
async request<T>(endpoint: string, options?: RequestInit) {
const res = await fetch(this.getStringURL(endpoint), {
method: 'GET',
...options,

@ -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<LoginFormValues>({
defaultValues: {
email: '',
password: '',
email: 'stepan.pilipenko@xmail.ru',
password: 'password_123',
},
// react-hook-form умеет работать со многими библиотеками
// валидации, мы используем yup

@ -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<RegistrFormValues> = 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 => {
}}
>
<Avatar sx={{ m: 1, bgcolor: 'info.main' }}>
<KeyIcon />
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Авторизация
Регистрация
</Typography>
<Box
component="form"
@ -135,7 +155,7 @@ export const RegisterPage = (): JSX.Element => {
fullWidth
sx={{ mt: 3, mb: 2 }}
>
Авторизация
Регистрация
</Button>
<Link component={RouterLink} to="/login">
Логин

@ -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 (
<Container maxWidth="lg">
<BackButton title="Профиль" text="< Главная" path="/" />
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 4 }}>
<Avatar
alt={currentUser?.name}
src={
currentUser?.avatarPath
? currentUser.avatarPath
: '/static/images/avatar/1.jpg'
}
alt={userData?.nickname}
src={userAvatar ? userAvatar : '/static/images/avatar/1.jpg'}
sx={{ width: 150, height: 150 }}
/>
<p>{currentUser?.name}</p>
<p>{currentUser?.about}</p>
<p>{userData?.nickname}</p>
<p>{userData?.email}</p>
<Stack>
<Button variant="outlined">Редактировать профиль</Button>
<Button variant="outlined" color="info">
Изменить аватар
<Button variant="outlined" onClick={handleClick}>
Выйти
</Button>
<Button variant="outlined" color="error">
Удалить аккаунт
</Button>
</Stack>
</Grid>
@ -37,3 +41,5 @@ export const UserPage = withProtection(() => {
</Container>
)
})
export const UserPage = withResponse(UserWithProtection, user, user.fetch.user)

@ -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<AuthState>) {
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<ResponseData, RegisterProps>(
`${authSlice.name}/fetchRegister`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/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<ResponseData, LoginProps>(
const fetchLogin = createAppAsyncThunk<ResponseData, LoginProps>(
`${authSlice.name}/fetchLogin`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
@ -92,7 +134,7 @@ export const fetchLogin = createAppAsyncThunk<ResponseData, LoginProps>(
}
)
export const fetchLogout = createAppAsyncThunk<ResponseData>(
const fetchLogout = createAppAsyncThunk<ResponseData>(
`${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()

@ -64,7 +64,7 @@ const selectProduct = (productId: number) =>
return products.find(product => product.id === productId)
})
export const fetchProducts = createAppAsyncThunk<ResponseData>(
const fetchProducts = createAppAsyncThunk<ResponseData>(
`${shopSlice.name}/fetchProducts`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {

@ -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<UserType>) => {
@ -54,11 +52,13 @@ const userSlice = createSlice({
},
})
export const fetchUser = createAppAsyncThunk<ResponseData>(
`${userSliceName}/fetchUser`,
const fetchUser = createAppAsyncThunk<ResponseData>(
`${userSlice.name}/fetchUser`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/user/')
const data = await networkApi.request<ResponseData>('/user/', {
method: 'POST',
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)

@ -0,0 +1 @@
export * from './token'

@ -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()
Loading…
Cancel
Save