diff --git a/package-lock.json b/package-lock.json index 9c41807f..f4f90c29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "react-dom": "18.2.0", "react-input-mask": "^2.0.4", "react-leaflet": "^4.2.1", + "react-loading-skeleton": "^3.4.0", "react-redux": "8.1.2", "react-router": "6.15.0", "react-router-dom": "6.15.0", @@ -17311,6 +17312,14 @@ "react-dom": "^18.0.0" } }, + "node_modules/react-loading-skeleton": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz", + "integrity": "sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-redux": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", @@ -33317,6 +33326,12 @@ "@react-leaflet/core": "^2.1.0" } }, + "react-loading-skeleton": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz", + "integrity": "sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==", + "requires": {} + }, "react-redux": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", diff --git a/package.json b/package.json index 0f551ffe..25fa73a9 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "react-dom": "18.2.0", "react-input-mask": "^2.0.4", "react-leaflet": "^4.2.1", + "react-loading-skeleton": "^3.4.0", "react-redux": "8.1.2", "react-router": "6.15.0", "react-router-dom": "6.15.0", diff --git a/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.module.scss b/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.module.scss new file mode 100644 index 00000000..ffc7909f --- /dev/null +++ b/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.module.scss @@ -0,0 +1,28 @@ +@use '../../../shared/styles/utils/variables' as var; + +.sk-page-controls { + width: 100%; + display: flex; + justify-content: space-between; + + &__dropdowns { + display: flex; + gap: 12px; + } + + &__dropdown { + width: 150px; + height: 36px; + } + + &__cards-controls { + display: flex; + gap: 10px; + } + + &__cards-control { + width: 36px; + height: 36px; + border-radius: 6px; + } +} diff --git a/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.stories.tsx b/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.stories.tsx new file mode 100644 index 00000000..236034bb --- /dev/null +++ b/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.stories.tsx @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from '@storybook/react' +import { type FC } from 'react' + +import { PageControlsSkeletons } from '@/components/PageControls/PageControlsSkeletons/PageControlsSkeletons' + +const StorybookWrapper: FC = () => { + return ( +
+ +
+ ) +} + +const meta = { + title: 'shared/PageControlsSkeletons', + component: StorybookWrapper, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.tsx b/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.tsx new file mode 100644 index 00000000..e15eacb4 --- /dev/null +++ b/src/components/PageControls/PageControlsSkeletons/PageControlsSkeletons.tsx @@ -0,0 +1,21 @@ +import { type FC } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' + +import styles from './PageControlsSkeletons.module.scss' + +export const PageControlsSkeletons: FC = () => { + return ( +
+
+ + +
+
    + + + +
+
+ ) +} diff --git a/src/components/PageDescription/PageDescription.tsx b/src/components/PageDescription/PageDescription.tsx index 737ebbb7..f38c8dff 100644 --- a/src/components/PageDescription/PageDescription.tsx +++ b/src/components/PageDescription/PageDescription.tsx @@ -1,4 +1,6 @@ -import { FC } from 'react' +import { type FC } from 'react' +import 'react-loading-skeleton/dist/skeleton.css' +import { useParams } from 'react-router-dom' import { getNoun } from '@/shared/libs/helpers/getNoun' import Breadcrumbs from '@/shared/ui/Breadcrumbs/Breadcrumbs' @@ -17,9 +19,11 @@ type Props = { * @param {string} heading - наименование-заголовок; */ export const PageDescription: FC = ({ count, heading }) => { + const { slug } = useParams() + const links = [ { heading: 'Главная', href: '/' }, - { heading: heading, href: '/categories/' + heading } + { heading: heading, href: '/categories/' + slug } ] return ( diff --git a/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.module.scss b/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.module.scss new file mode 100644 index 00000000..61a7cd45 --- /dev/null +++ b/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.module.scss @@ -0,0 +1,17 @@ +@use '../../../shared/styles/utils/variables' as var; + +.sk-content { + width: 330px; + height:30px +} + +.sk-content__description { + display: flex; + flex-direction: column; + align-self: flex-start; + gap: 10px; +} + +.sk-content__breadcrumbs { + height: 16px; +} diff --git a/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.stories.tsx b/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.stories.tsx new file mode 100644 index 00000000..ea2a2d68 --- /dev/null +++ b/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.stories.tsx @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from '@storybook/react' +import { type FC } from 'react' + +import { PageDescriptionSkeleton } from '@/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton' + +const StorybookWrapper: FC = () => { + return ( +
+ +
+ ) +} + +const meta = { + title: 'shared/PageDescriptionSkeleton', + component: StorybookWrapper, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.tsx b/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.tsx new file mode 100644 index 00000000..9de55a5e --- /dev/null +++ b/src/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.tsx @@ -0,0 +1,15 @@ +import { type FC } from 'react' +import Skeleton from 'react-loading-skeleton' + +import styles from '@/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton.module.scss' + +export const PageDescriptionSkeleton: FC = () => { + return ( +
+
+ +
+ +
+ ) +} diff --git a/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.module.scss b/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.module.scss new file mode 100644 index 00000000..eee44f42 --- /dev/null +++ b/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.module.scss @@ -0,0 +1,6 @@ +.sk-category-list__item { + display: flex; + min-height: 45px; + border-radius: 5px; + margin-bottom: 5px; +} diff --git a/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.stories.tsx b/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.stories.tsx new file mode 100644 index 00000000..a17a45df --- /dev/null +++ b/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.stories.tsx @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from '@storybook/react' +import { type FC } from 'react' + +import { CategoryItemSkeleton } from '@/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton' + +const StorybookWrapper: FC = () => { + return ( +
+ +
+ ) +} + +const meta = { + title: 'features/CategoryItemSkeleton', + component: StorybookWrapper, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.tsx b/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.tsx new file mode 100644 index 00000000..f754ce2e --- /dev/null +++ b/src/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.tsx @@ -0,0 +1,8 @@ +import { type FC } from 'react' +import Skeleton from 'react-loading-skeleton' + +import styles from '@/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton.module.scss' + +export const CategoryItemSkeleton: FC = () => { + return +} diff --git a/src/pages/ProductsPage/ProductsPage.tsx b/src/pages/ProductsPage/ProductsPage.tsx index bcd4797d..a6778b27 100644 --- a/src/pages/ProductsPage/ProductsPage.tsx +++ b/src/pages/ProductsPage/ProductsPage.tsx @@ -5,17 +5,20 @@ import { useLocation } from 'react-router' import { selectFilterProducts, selectFilterQuantity } from '@/components/Dropdown/selectors/selectors' import { setFilterProducts, setProductQuantityFilter } from '@/components/Dropdown/slice/filtersSlice' import { PageControls } from '@/components/PageControls/PageControls' +import { PageControlsSkeletons } from '@/components/PageControls/PageControlsSkeletons/PageControlsSkeletons' import { PageDescription } from '@/components/PageDescription/PageDescription' +import { PageDescriptionSkeleton } from '@/components/PageDescription/PageDescriptionSkeleton/PageDescriptionSkeleton' import { Pagination } from '@/components/Pagination/Pagination' import WrapperForMainContent from '@/components/WrapperForMainContent/WrapperForMainContent' import { selectCategorySlug } from '@/entities/Category/selectors/categorySelectors' import { TOTAL_PAGES } from '@/mockData/productsPageOptions' -import { getProductsOfCategorySelector } from '@/pages/ProductsPage/selectors/selectors' +import { getLoading, getProductsOfCategorySelector } from '@/pages/ProductsPage/selectors/selectors' import { getProducts } from '@/pages/ProductsPage/services/getProducts' -import { ITEMS_PER_PAGE_OPTION, SORT_OPTION } from '@/shared/constants/constants' +import { ITEMS_PER_PAGE_OPTION, NUMBER_OF_PRODUCTS, SORT_OPTION } from '@/shared/constants/constants' import { useAppDispatch } from '@/shared/libs/hooks/store' import { ECardView } from '@/shared/model/types/common' import { CategoryList } from '@/widgets/CategoryList/CategoryList' +import { ProductSkeleton } from '@/widgets/ProductItem/ProductSkeleton/ProductSkeleton' import { ProductsList } from '@/widgets/ProductsList/ProductsList' import styles from './ProductsPage.module.scss' @@ -46,6 +49,8 @@ export const ProductsPage = () => { const selectQuantityFilter = useSelector(selectFilterQuantity) const filterQuantity = selectQuantityFilter ? `&limit=${selectQuantityFilter.value}` : '' + const isLoading = useSelector(getLoading) + const handleSortChange: React.ChangeEventHandler = event => { const selectedOption = event.target.value const setCategoryFilters = SORT_OPTION.find(item => item.name === selectedOption) @@ -83,12 +88,25 @@ export const ProductsPage = () => { return ( <> - + {isLoading ? ( + + ) : ( + + )}
- {categoriesProducts.results.length > 0 ? ( + {isLoading ? ( + <> + + {Array(NUMBER_OF_PRODUCTS) + .fill(0) + .map((_, i) => ( + + ))} + + ) : categoriesProducts.results.length > 0 ? ( <> { return state.categoryProduct.productsData } + +export const getLoading = (state: RootState) => { + return state.categoryProduct.isLoading +} diff --git a/src/shared/constants/constants.ts b/src/shared/constants/constants.ts index ecb8f8cc..f58a7f24 100644 --- a/src/shared/constants/constants.ts +++ b/src/shared/constants/constants.ts @@ -93,3 +93,7 @@ export const ITEMS_PER_PAGE_OPTION = [ { name: '75', value: '75' }, { name: '100', value: '100' } ] + +//For Skeleton +export const NUMBER_OF_CATEGORY_LINES = 15 +export const NUMBER_OF_PRODUCTS = 15 diff --git a/src/widgets/CategoryList/CategoryList.tsx b/src/widgets/CategoryList/CategoryList.tsx index 8e02ec3c..778b9787 100644 --- a/src/widgets/CategoryList/CategoryList.tsx +++ b/src/widgets/CategoryList/CategoryList.tsx @@ -1,9 +1,12 @@ -import { FC, useEffect } from 'react' +import { type FC, useEffect } from 'react' import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { selectCategorySlug } from '@/entities/Category/selectors/categorySelectors' import { CategoryItem } from '@/features/CategoryItem/CategoryItem' +import { CategoryItemSkeleton } from '@/features/CategoryItem/CategoryItemSkeleton/CategoryItemSkeleton' +import { getLoading } from '@/pages/ProductsPage/selectors/selectors' +import { NUMBER_OF_CATEGORY_LINES } from '@/shared/constants/constants' import { useAppDispatch } from '@/shared/libs/hooks/store' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import { getCategoryBranchesSelector, getCategorySelector } from '@/widgets/CategoryList/selectors/selectors' @@ -24,14 +27,12 @@ export const CategoryList: FC = () => { const { slug } = useParams() - useEffect(() => { - dispatch(getCategoryBranches(slug)) - }, []) + const isLoading = useSelector(getLoading) useEffect(() => { - dispatch(getCategoryBranches(categorySlug)) + dispatch(getCategoryBranches(slug)) dispatch(getCategories()) - }, [categorySlug]) + }, [categorySlug, slug]) return (
@@ -39,7 +40,11 @@ export const CategoryList: FC = () => { Категории
    - {categoryBranches.branches?.length > 0 + {isLoading + ? Array(NUMBER_OF_CATEGORY_LINES) + .fill(0) + .map((_, i) => ) + : categoryBranches.branches?.length > 0 ? categoryBranches.branches.map(item => ( { + return ( +
    + +
    + ) +} + +const meta = { + title: 'shared/ProductSkeleton', + component: StorybookWrapper, + tags: ['autodocs'] +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} diff --git a/src/widgets/ProductItem/ProductSkeleton/ProductSkeleton.tsx b/src/widgets/ProductItem/ProductSkeleton/ProductSkeleton.tsx new file mode 100644 index 00000000..cdcec39d --- /dev/null +++ b/src/widgets/ProductItem/ProductSkeleton/ProductSkeleton.tsx @@ -0,0 +1,30 @@ +import { type FC } from 'react' +import Skeleton from 'react-loading-skeleton' +import 'react-loading-skeleton/dist/skeleton.css' + +import styles from './ProductSkeleton.module.scss' + +export const ProductSkeleton: FC = () => { + return ( +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    + +
    +
    +
    +
    + ) +}