Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api: Получение категорий для компонента Header #183

Merged
merged 4 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app/providers/SroreProvider/config/StateSchema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { CategorySchema } from '@/entities/Category/types/types'
import { LoginSchema } from '@/features/login/model/types/types'
import { ApiInstance } from '@/shared/api/api'

export interface StateSchema {
login: LoginSchema
category: CategorySchema
}

export interface ThunkExtraArg {
Expand Down
6 changes: 5 additions & 1 deletion src/app/providers/SroreProvider/config/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { configureStore, ReducersMapObject } from '@reduxjs/toolkit'
import { loginReducer } from '@/features/login/model/slice/loginSlice'
import { StateSchema, ThunkExtraArg } from './StateSchema'
import { $api } from '@/shared/api/api'
import categorySlice from '@/entities/Category/slice/categorySlice'

export type RootState = StateSchema

const rootReducer: ReducersMapObject<StateSchema> = {
login: loginReducer
login: loginReducer,
category: categorySlice
}

export function createReduxStore(initialState: StateSchema) {
Expand Down
5 changes: 5 additions & 0 deletions src/app/router/AppRouter/ui/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export const AppRouter = createBrowserRouter([
path: Routes.PRODUCTS_ID,
element: <ProductsPage />
},
// Добавил как временную заглушку пока не будет страницы категории
{
path: Routes.CATEGORIES + '/:slug',
element: <ProductsPage />
},
{
path: Routes.LOGIN,
element: <LoginPage />
Expand Down
21 changes: 15 additions & 6 deletions src/components/ContextMenuElement/ContextMenuElement.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { FC, ReactNode, useEffect, useRef, useState } from 'react'
import styles from './contextMenuElement.module.scss'

import { type FC, ReactNode, useEffect, useRef, useState } from 'react'
import { LEFT_POSITION, RIGHT_POSITION } from '@/shared/constants/constants'
import styles from './contextMenuElement.module.scss'

type TContextMenuElement = {
readonly children: ReactNode
Expand All @@ -14,8 +13,12 @@ type TContextMenuElement = {
* @param {string} className - нужно для изменения некоторых css-параметров
* @param {string} type - нужно для определения, к какому краю прилегает текст
*/

const ContextMenuElement: FC<TContextMenuElement> = ({ children, content, className, type = LEFT_POSITION }) => {
const ContextMenuElement: FC<TContextMenuElement> = ({
children,
content,
className,
type = LEFT_POSITION
}) => {
const ref = useRef<HTMLDivElement>(null)
const [top, setTop] = useState(0)

Expand Down Expand Up @@ -43,11 +46,17 @@ const ContextMenuElement: FC<TContextMenuElement> = ({ children, content, classN
}

const contextMenu = (
<div className={`${styles['context-menu']} `} style={(type === RIGHT_POSITION && contextStyle) || { top: top }}>
<div
className={`${styles['context-menu']} `}
style={(type === RIGHT_POSITION && contextStyle) || { top: top }}>
{content}
</div>
)

// @TODO: Добавить обработчик клика по элементу из ContextMenuElement
// От таких кликов меню должно скрываться
// https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/188

return (
<div
ref={ref}
Expand Down
2 changes: 2 additions & 0 deletions src/components/header/header.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@
list-style-type: none;
padding: 20px 40px 20px 20px;
row-gap: 15px;
max-height: 320px;
overflow-y: scroll;
}

&__context-menu-item {
Expand Down
69 changes: 28 additions & 41 deletions src/components/header/header.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я думал header уже переделан по fsd

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#156
Еще в работе

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { coreBaseData } from '@/mockData/coreBaseData'
import { useMemo } from 'react'
import { useEffect, useMemo } from 'react'
import classNames from 'classnames'
import Logo from '../../shared/ui/logo/Logo'
import ArrowIcon from '@/assets/icons/arrow.svg'
Expand All @@ -8,16 +8,25 @@ import ContextMenuElement from '../ContextMenuElement/ContextMenuElement'
import HeaderAccount from '../HeaderAccount/HeaderAccount'
import { PHONE_NUMBER } from '@/shared/constants/constants'
import { headerAccountData } from '@/mockData/headerAccountData'
import { catalogListData } from '@/mockData/catalogListData'
import CatalogLink from '../CatalogLink/CatalogLink'
import { Routes } from '@/shared/config/routerConfig/routes'
import { CatalogLinksId } from '@/shared/config/catalogLinks/catalogLinks'
import Link from '@/shared/ui/Link/Link'
import IconCategories from '@/assets/icons/IconCategories.svg'
import styles from './header.module.scss'
import SearchProduct from '@/features/SearchProduct'
import { linkItems } from '@/mockData/catalogListData'
import styles from './header.module.scss'
import { useDispatch, useSelector } from 'react-redux'
import { fetchCategories } from '@/entities/Category/slice/categorySlice'
import { AppDispatch } from '@/app/providers/SroreProvider/config/store'
import { selectCategories, selectDisplayedCategories } from '@/entities/Category/selectors/categorySelectors'
import CatalogNodeItem from '@/widgets/CatalogNodeItem/CatalogNodeItem'
import NavigationLink from '@/widgets/NavigationLink/NavigationLink'

function Header() {
const dispatch = useDispatch<AppDispatch>()
const categories = useSelector(selectCategories)
const displayedCategories = useSelector(selectDisplayedCategories)

const aboutUsNode = useMemo(
() => (
<ul className={styles['header__context-menu-list']}>
Expand Down Expand Up @@ -91,51 +100,32 @@ function Header() {
const catalogNode = useMemo(
() => (
<ul className={styles['header__context-menu-list']}>
{catalogListData.map((item, index) => (
<li key={index} className={styles['header__context-menu-item']}>
<Link to={`${Routes}${item.slug}`} className={styles['header__context-menu-link']}>
{item.name}
</Link>
</li>
{categories.map(category => (
<CatalogNodeItem key={category.id} slug={category.slug} name={category.name} />
))}
</ul>
),
[]
[categories]
)

useEffect(() => {
dispatch(fetchCategories())
}, [dispatch])

return (
<header className={styles.header}>
<div className={styles.header__container}>
<div className={styles['header__row-one']}>
<nav className={styles.header__nav}>
<ul className={styles.header__list}>
{/* TODO замапить список ссылок из конфига чтобы не засорять код
https://github.com/Studio-Yandex-Practicum/maxboom_frontend/issues/125 */}
<li className={styles.header__item}>
<ContextMenuElement className={styles.header__item} content={aboutUsNode}>
О нас
</ContextMenuElement>
</li>
<li className={styles.header__item}>
<Link to={Routes.BLOG} className={styles.header__link}>
Блог
</Link>
</li>
<li className={styles.header__item}>
<Link to="" className={styles.header__link}>
Новости
</Link>
</li>
<li className={styles.header__item}>
<Link to="" className={styles.header__link}>
Отзывы о магазине
</Link>
</li>
<li className={styles.header__item}>
<Link to="" className={styles.header__link}>
Контакты
</Link>
</li>
{linkItems.map(item => (
<NavigationLink key={item.index} label={item.label} to={item.to} />
))}
<ContextMenuElement className={styles.header__item} content={supportNode}>
<LightningIcon className={classNames(styles.header__icon, styles.help_icon)} />
Помощь
Expand Down Expand Up @@ -165,7 +155,6 @@ function Header() {

<div className={styles['header__row-three']}>
<ContextMenuElement content={catalogNode}>
{/* @TODO: вставить путь когда будет роут */}
<CatalogLink to="" className={styles['header__catalog-link_main']}>
<div className={styles['header__catalog-wrapper']}>
<IconCategories className={styles['header__svg']} />
Expand All @@ -175,13 +164,11 @@ function Header() {
</ContextMenuElement>

<div className={styles['header__tags']}>
<CatalogLink to={Routes.PRODUCTS + CatalogLinksId.TRANSMIT}>GPS-треккеры</CatalogLink>
<CatalogLink to={Routes.PRODUCTS + CatalogLinksId.GPS_TRACK}>SSD-накопители</CatalogLink>
<CatalogLink to={Routes.PRODUCTS + CatalogLinksId.SSD}>Автозапчасти</CatalogLink>
<CatalogLink to={Routes.PRODUCTS + CatalogLinksId.AUTO_PARTS}>
Автомобильные зарядные устройства
</CatalogLink>
<CatalogLink to={Routes.PRODUCTS + CatalogLinksId.CAR_CHARGES}>Автосканеры</CatalogLink>
{displayedCategories.map(category => (
<CatalogLink key={category.id} to={`${Routes.CATEGORIES}/${category.slug}`}>
{category.name}
</CatalogLink>
))}
</div>
</div>
</div>
Expand Down
4 changes: 4 additions & 0 deletions src/entities/Category/selectors/categorySelectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { RootState } from '@/app/providers/SroreProvider/config/store'

export const selectCategories = (state: RootState) => state.category.categories
export const selectDisplayedCategories = (state: RootState) => state.category.displayedCategories
47 changes: 47 additions & 0 deletions src/entities/Category/slice/categorySlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types'
import { ThunkConfig } from '@/app/providers/SroreProvider/config/StateSchema'
import { Category, CategorySchema } from '../types/types'
import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify'
import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle'

const initialState: CategorySchema = {
categories: [],
displayedCategories: [],
error: undefined
}

export const fetchCategories = createAsyncThunk<Category[], void, ThunkConfig<ApiError>>(
'category/fetchCategories',
async (_, thunkAPI) => {
const { rejectWithValue, extra } = thunkAPI

try {
const response = await extra.api.get(`api/${ApiRoutes.CATEGORIES}`)
return response.data as Category[]
} catch (error) {
return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.AUTH_ERROR))
}
}
)

const categorySlice = createSlice({
name: 'category',
initialState,
reducers: {},
extraReducers: builder => {
builder
.addCase(fetchCategories.pending, state => {
state.error = undefined
})
.addCase(fetchCategories.fulfilled, (state, action) => {
state.categories = action.payload
state.displayedCategories = action.payload.filter((c: Category) => c.is_visible_on_main === true)
})
.addCase(fetchCategories.rejected, (state, { payload }) => {
state.error = rejectedPayloadHandle(payload)
})
}
})

export default categorySlice.reducer
17 changes: 17 additions & 0 deletions src/entities/Category/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface CategorySchema {
categories: Category[]
displayedCategories: Category[]
error?: string | string[]
}

export interface Category {
id?: number
name: string
slug: string
branches?: Category[]
root?: Category
is_prohibited?: boolean
is_visible_on_main?: boolean
image?: string
type?: 'category'
}
8 changes: 8 additions & 0 deletions src/mockData/catalogListData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { CatalogLinksId } from '@/shared/config/catalogLinks/catalogLinks'
import { Routes } from '@/shared/config/routerConfig/routes'

export const linkItems = [
{ index: 0, label: 'Блог', to: Routes.BLOG },
{ index: 1, label: 'Новости', to: '' },
{ index: 2, label: 'Отзывы о магазине', to: '' },
{ index: 3, label: 'Контакты', to: '' }
]

export const catalogListData = [
{
Expand Down
3 changes: 2 additions & 1 deletion src/shared/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum ApiRoutes {
LOGIN = 'token/login',
LOGOUT = 'token/logout'
LOGOUT = 'token/logout',
CATEGORIES = 'catalogue/category'
}

export enum ApiErrorTypes {
Expand Down
18 changes: 18 additions & 0 deletions src/widgets/CatalogNodeItem/CatalogNodeItem.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@use '../../shared/styles/utils/variables' as var;

.link {
color: var.$body-color;
text-decoration: none;
white-space: nowrap;

&:hover {
color: var.$theme-secondary-color;
}
}

.li {
margin: 0;
padding: 0;
font-weight: 400;
font-size: 13px;
}
29 changes: 29 additions & 0 deletions src/widgets/CatalogNodeItem/CatalogNodeItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// CatalogNodeItem.stories.tsx
import { Meta, StoryObj } from '@storybook/react'
import CatalogNodeItem from './CatalogNodeItem'

const meta = {
title: 'widgets/CatalogNodeItem',
component: CatalogNodeItem,
parameters: {
layout: 'centered'
}
} as Meta<typeof CatalogNodeItem>

export default meta

type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
slug: 'category-slug',
name: 'Category Name'
}
}

export const CustomCategory: Story = {
args: {
slug: 'custom-slug',
name: 'Custom Category'
}
}
19 changes: 19 additions & 0 deletions src/widgets/CatalogNodeItem/CatalogNodeItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Category } from '@/entities/Category/types/types'
import { Routes } from '@/shared/config/routerConfig/routes'
import Link from '@/shared/ui/Link/Link'
import styles from './CatalogNodeItem.module.scss'

/**
* Компонент ссылки на раздел каталога для выпадающего списка меню "Все категории"
*/
const CatalogNodeItem = ({ slug, name }: Category) => {
return (
<li className={styles.li}>
<Link to={`${Routes.CATEGORIES}/${slug}`} className={styles.link}>
{name}
</Link>
</li>
)
}

export default CatalogNodeItem
16 changes: 16 additions & 0 deletions src/widgets/NavigationLink/NavigationLink.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@use '../../shared/styles/utils/variables' as var;

.li {
margin: 0;
padding: 0;
font-weight: 600;
font-size: 13px;
color: var.$white;
display: flex;
align-items: center;
}

.link {
color: var.$white;
text-decoration: none;
}
Loading
Loading