From 425451c75612b2504dfc3e1f37b46cc2c782246a Mon Sep 17 00:00:00 2001 From: Yulia Avramenko Date: Fri, 2 Feb 2024 12:53:45 +0300 Subject: [PATCH 1/2] #140-api-inserted-to-BlogBlock --- .../StoreProvider/config/StateSchema.ts | 2 ++ .../providers/StoreProvider/config/store.ts | 4 ++- src/entities/BlogCard/BlogCard.module.scss | 16 +++++---- src/entities/BlogCard/BlogCard.stories.tsx | 11 +++---- src/entities/BlogCard/BlogCard.tsx | 33 ++++++++++++++----- src/entities/NewsCard/NewsCard.module.scss | 1 + src/entities/NewsCard/NewsCard.tsx | 4 +-- src/shared/api/types.ts | 3 +- src/shared/constants/constants.ts | 8 +++++ .../BlogBlock/model/selectors/selectiors.ts | 6 ++++ .../BlogBlock/model/services/getBlogPosts.ts | 23 +++++++++++++ .../BlogBlock/model/slice/blogPostsSlice.ts | 29 ++++++++++++++++ src/widgets/BlogBlock/model/types/types.ts | 32 ++++++++++++++++++ .../BlogBlock/ui/BlogBlock.stories.tsx | 3 +- src/widgets/BlogBlock/ui/BlogBlock.tsx | 18 +++++++--- .../NewsBlock/model/selectors/selectors.ts | 1 + .../NewsBlock/model/services/getShopNews.ts | 3 +- .../NewsBlock/model/slice/shopNewsSlice.ts | 3 +- 18 files changed, 167 insertions(+), 33 deletions(-) create mode 100644 src/widgets/BlogBlock/model/selectors/selectiors.ts create mode 100644 src/widgets/BlogBlock/model/services/getBlogPosts.ts create mode 100644 src/widgets/BlogBlock/model/slice/blogPostsSlice.ts create mode 100644 src/widgets/BlogBlock/model/types/types.ts diff --git a/src/app/providers/StoreProvider/config/StateSchema.ts b/src/app/providers/StoreProvider/config/StateSchema.ts index 825738f8..d64c1785 100644 --- a/src/app/providers/StoreProvider/config/StateSchema.ts +++ b/src/app/providers/StoreProvider/config/StateSchema.ts @@ -5,6 +5,7 @@ import { BrandSchema } from '@/widgets/BrandBlock/types/types' import { ApiInstance } from '@/shared/api/api' import { ShopNewsSchema } from '@/widgets/NewsBlock/model/types/types' import { StoreReviewsSchema } from '@/widgets/ReviewsBlock/model/types/types' +import { IBlogPostsSchema } from '@/widgets/BlogBlock/model/types/types' export interface StateSchema { login: LoginSchema @@ -13,6 +14,7 @@ export interface StateSchema { brand: BrandSchema searchResult: SearchResultSchema shopNews: ShopNewsSchema + blogPosts: IBlogPostsSchema } export interface ThunkExtraArg { diff --git a/src/app/providers/StoreProvider/config/store.ts b/src/app/providers/StoreProvider/config/store.ts index cfa74dde..89bc795e 100644 --- a/src/app/providers/StoreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -7,6 +7,7 @@ import brandSlice from '@/widgets/BrandBlock/slice/brandSlice' import searchProductSlice from '@/features/SearchProduct/slice/searchProductSlice' import { storeReviewsReducer } from '@/widgets/ReviewsBlock/model/slice/reviewsSlice' import { shopNewsReducer } from '@/widgets/NewsBlock/model/slice/shopNewsSlice' +import { blogPostsReducer } from '@/widgets/BlogBlock/model/slice/blogPostsSlice' export type RootState = StateSchema @@ -16,7 +17,8 @@ const rootReducer: ReducersMapObject = { brand: brandSlice, searchResult: searchProductSlice, storeReviews: storeReviewsReducer, - shopNews: shopNewsReducer + shopNews: shopNewsReducer, + blogPosts: blogPostsReducer } export function createReduxStore(initialState: RootState) { diff --git a/src/entities/BlogCard/BlogCard.module.scss b/src/entities/BlogCard/BlogCard.module.scss index 5ee6b802..a6175bb4 100644 --- a/src/entities/BlogCard/BlogCard.module.scss +++ b/src/entities/BlogCard/BlogCard.module.scss @@ -1,6 +1,7 @@ @use '@/app/styles/index' as var; .card { + max-width: 340px; min-width: 340px; position: relative; transition: transform 0.3s ease-in-out; @@ -14,12 +15,6 @@ transition: transform 0.3s ease-in-out; } - img { - border-radius: 6px; - transition: transform 0.3s ease-in-out; - scroll-snap-align: start; - } - .heading { font-size: #{'min(max(14px, 1.2vw), 16px)'}; @@ -32,5 +27,14 @@ span { color: var.$body-color-light-grey; } + + .img { + height: 462px; + width: 100%; + border-radius: 6px; + transition: transform 0.3s ease-in-out; + scroll-snap-align: start; + object-fit: cover; + } } \ No newline at end of file diff --git a/src/entities/BlogCard/BlogCard.stories.tsx b/src/entities/BlogCard/BlogCard.stories.tsx index 52f4421e..d571daec 100644 --- a/src/entities/BlogCard/BlogCard.stories.tsx +++ b/src/entities/BlogCard/BlogCard.stories.tsx @@ -16,12 +16,9 @@ type Story = StoryObj export const Default: Story = { args: { - card: { - id: 1, - src: Img1, - alt: 'Покупай и не жди. До -50% на весь электротранспорт!', - title: 'Покупай и не жди. До -50% на весь электротранспорт!', - date: '8 Мая, 2022' - } + id: 1, + image: Img1, + title: 'Покупай и не жди. До -50% на весь электротранспорт!', + date: '2022-07-8' } } diff --git a/src/entities/BlogCard/BlogCard.tsx b/src/entities/BlogCard/BlogCard.tsx index f4f64202..c189ae9b 100644 --- a/src/entities/BlogCard/BlogCard.tsx +++ b/src/entities/BlogCard/BlogCard.tsx @@ -1,26 +1,41 @@ -import { FC } from 'react' -import { TCard } from '@/models/CardModel' +import { FC, useMemo } from 'react' import styles from './BlogCard.module.scss' import Link from '@/shared/ui/Link/Link' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' +import NoImage from '@/assets/icons/image-not-found-icon.svg' -export type Props = { - card: TCard +type Props = { + id: number + image: string + title: string + date: string } /** * Карточка из блока блог - * @param {TCard} card - параметры карточки из блога + * @param {Props} card - параметры карточки из блога */ -const BlogCard: FC = ({ card }) => { +const BlogCard: FC = ({ image, date, title }) => { + const newDate = useMemo(() => { + const _parsedDate = new Date(date) + const year = _parsedDate.getFullYear() + const formatter = new Intl.DateTimeFormat('ru', { month: 'long', day: 'numeric' }).format(_parsedDate) + + return `${formatter}, ${year}` + }, [date]) + return ( - {card.alt} + {image ? ( + {'новость'} + ) : ( + + )} - {card.title} + {title} - {card.date} + {newDate} ) } diff --git a/src/entities/NewsCard/NewsCard.module.scss b/src/entities/NewsCard/NewsCard.module.scss index 8f3ed61f..52fad4c9 100644 --- a/src/entities/NewsCard/NewsCard.module.scss +++ b/src/entities/NewsCard/NewsCard.module.scss @@ -48,5 +48,6 @@ border-radius: 6px; transition: transform 0.3s ease-in-out; scroll-snap-align: start; + object-fit: cover; } } \ No newline at end of file diff --git a/src/entities/NewsCard/NewsCard.tsx b/src/entities/NewsCard/NewsCard.tsx index 5a581025..7c403a95 100644 --- a/src/entities/NewsCard/NewsCard.tsx +++ b/src/entities/NewsCard/NewsCard.tsx @@ -4,7 +4,7 @@ import Link from '@/shared/ui/Link/Link' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import NoImage from '@/assets/icons/image-not-found-icon.svg' -export type Props = { +type Props = { id: number image: string date: string @@ -13,7 +13,7 @@ export type Props = { /** * Карточка из блока группы новостей - * @param {TCard} card - параметры карточки из группы новостей + * @param {Props} card - параметры карточки из группы новостей */ const NewsCard: FC = ({ image, date, title }) => { diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 7ec8a5dc..4e9fc3cb 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -5,7 +5,8 @@ export enum ApiRoutes { SEARCH = 'search', STORE_REVIEWS = 'store-reviews', CATEGORIES = 'catalogue/category', - SHOP_NEWS = 'shopnews' + SHOP_NEWS = 'shopnews', + BLOG_POSTS = 'shopblog/posts' } export enum ApiErrorTypes { diff --git a/src/shared/constants/constants.ts b/src/shared/constants/constants.ts index ff3f1a8a..82b96e43 100644 --- a/src/shared/constants/constants.ts +++ b/src/shared/constants/constants.ts @@ -31,3 +31,11 @@ export const MAX_PRODUCTS_NUMBER: number = 99 //for BrandBlock component export const BRANDS_FOR_MAIN_NUMBER: number = 6 + +// Actions +export const ACTION_GET_SHOP_NEWS = 'get-shop-news' +export const ACTION_GET_BLOG_POSTS = 'get-blog-posts' + +// Reducers +export const REDUCER_SHOP_NEWS = 'shopNews' +export const REDUCER_BLOG_POSTS = 'shopBlogPosts' diff --git a/src/widgets/BlogBlock/model/selectors/selectiors.ts b/src/widgets/BlogBlock/model/selectors/selectiors.ts new file mode 100644 index 00000000..20da3eba --- /dev/null +++ b/src/widgets/BlogBlock/model/selectors/selectiors.ts @@ -0,0 +1,6 @@ +import { StateSchema } from '@/app/providers/StoreProvider' + +export const getBlogPostsSelector = (state: StateSchema) => { + //console.log('Selector BlogPosts', state.blogPosts.posts) + return state.blogPosts.posts +} diff --git a/src/widgets/BlogBlock/model/services/getBlogPosts.ts b/src/widgets/BlogBlock/model/services/getBlogPosts.ts new file mode 100644 index 00000000..cb844975 --- /dev/null +++ b/src/widgets/BlogBlock/model/services/getBlogPosts.ts @@ -0,0 +1,23 @@ +import { createAsyncThunk } from '@reduxjs/toolkit' +import { ThunkConfig } from '@/app/providers/StoreProvider/config/StateSchema' +import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' +import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' +import { IBlogPostData } from '../types/types' +import { ACTION_GET_BLOG_POSTS } from '@/shared/constants/constants' + +// export const getStoreReviews = createAsyncThunk>( +export const getBlogPosts = createAsyncThunk>( + //void1- выходные данные, void2- входные данные , thunkConfig- тип store + ACTION_GET_BLOG_POSTS, // action type, первый аргумент + async (_, thunkAPI) => { + // второй аргумент- асинхронная функция , кот вызовет dispatch в компоненте + const { rejectWithValue, extra } = thunkAPI + try { + const { data } = await extra.api.get(ApiRoutes.BLOG_POSTS) + + return data.results + } catch (error) { + return rejectWithValue(apiErrorIdentify(error, ApiErrorTypes.DATA_EMPTY_ERROR)) + } + } +) diff --git a/src/widgets/BlogBlock/model/slice/blogPostsSlice.ts b/src/widgets/BlogBlock/model/slice/blogPostsSlice.ts new file mode 100644 index 00000000..c60a5848 --- /dev/null +++ b/src/widgets/BlogBlock/model/slice/blogPostsSlice.ts @@ -0,0 +1,29 @@ +import { createSlice } from '@reduxjs/toolkit' +import { IBlogPostsSchema } from '../types/types' +import { getBlogPosts } from '../services/getBlogPosts' +import { REDUCER_BLOG_POSTS } from '@/shared/constants/constants' + +const initialState: IBlogPostsSchema = { + isLoading: false, + posts: [] +} + +export const blogPostsSlice = createSlice({ + name: REDUCER_BLOG_POSTS, + initialState, + reducers: {}, + extraReducers: builder => { + builder + .addCase(getBlogPosts.pending, state => { + state.isLoading = true + }) + .addCase(getBlogPosts.fulfilled, (state, { payload }) => { + state.isLoading = false + state.posts = payload + }) + .addCase(getBlogPosts.rejected, state => { + state.isLoading = false + }) + } +}) +export const { actions: blogPostsActions, reducer: blogPostsReducer } = blogPostsSlice diff --git a/src/widgets/BlogBlock/model/types/types.ts b/src/widgets/BlogBlock/model/types/types.ts new file mode 100644 index 00000000..baf4e95a --- /dev/null +++ b/src/widgets/BlogBlock/model/types/types.ts @@ -0,0 +1,32 @@ +export interface IPaginatedResponse { + count: number + previous: string + next: string + results: T[] +} +export interface IBlogTagData { + name: string +} +export interface TBlogCategoryLight { + title: string + slug: string +} +export interface IBlogPostData { + id: number + title: string + text: string + pub_date: string + author: string + image: string + category: TBlogCategoryLight + tags: IBlogTagData[] + views: number + slug: string + meta_title: string + meta_description: string +} + +export interface IBlogPostsSchema { + isLoading: boolean + posts: IBlogPostData[] +} diff --git a/src/widgets/BlogBlock/ui/BlogBlock.stories.tsx b/src/widgets/BlogBlock/ui/BlogBlock.stories.tsx index 1c9ee805..dae2de1e 100644 --- a/src/widgets/BlogBlock/ui/BlogBlock.stories.tsx +++ b/src/widgets/BlogBlock/ui/BlogBlock.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react' -import styles from './BlogBlock.module.scss' import { FC } from 'react' +import styles from './BlogBlock.module.scss' + import BlogBlock from './BlogBlock' const StorybookWrapper: FC = () => { diff --git a/src/widgets/BlogBlock/ui/BlogBlock.tsx b/src/widgets/BlogBlock/ui/BlogBlock.tsx index a7188d5e..63d4c6d6 100644 --- a/src/widgets/BlogBlock/ui/BlogBlock.tsx +++ b/src/widgets/BlogBlock/ui/BlogBlock.tsx @@ -1,10 +1,13 @@ -import { FC } from 'react' +import { FC, useEffect } from 'react' +import { getBlogPosts } from '../model/services/getBlogPosts' +import { useAppDispatch } from '@/shared/libs/hooks/store' +import { getBlogPostsSelector } from '../model/selectors/selectiors' +import { useSelector } from 'react-redux' import IconLink from '@/assets/icons/IconLink' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' import Link from '@/shared/ui/Link/Link' import styles from './BlogBlock.module.scss' import Scroll from '@/shared/ui/Scroll/Scroll' -import { blogData } from '@/mockData/blogData' import BlogCard from '@/entities/BlogCard/BlogCard' /** @@ -12,6 +15,13 @@ import BlogCard from '@/entities/BlogCard/BlogCard' */ const BlogBlock: FC = () => { + const dispatch = useAppDispatch() + const posts = useSelector(getBlogPostsSelector) + + useEffect(() => { + dispatch(getBlogPosts()) + }, []) + return (
@@ -22,8 +32,8 @@ const BlogBlock: FC = () => {
- {blogData.map(item => ( - + {posts.map(item => ( + ))}
diff --git a/src/widgets/NewsBlock/model/selectors/selectors.ts b/src/widgets/NewsBlock/model/selectors/selectors.ts index bcd58e38..c7bc5c60 100644 --- a/src/widgets/NewsBlock/model/selectors/selectors.ts +++ b/src/widgets/NewsBlock/model/selectors/selectors.ts @@ -1,5 +1,6 @@ import { StateSchema } from '@/app/providers/StoreProvider' export const getShopNewsSelector = (state: StateSchema) => { + // console.log('Selector ShopNews', state.shopNews.news) return state.shopNews.news } diff --git a/src/widgets/NewsBlock/model/services/getShopNews.ts b/src/widgets/NewsBlock/model/services/getShopNews.ts index 101275d7..25843a43 100644 --- a/src/widgets/NewsBlock/model/services/getShopNews.ts +++ b/src/widgets/NewsBlock/model/services/getShopNews.ts @@ -3,11 +3,12 @@ import { ThunkConfig } from '@/app/providers/StoreProvider/config/StateSchema' import { ApiError, ApiErrorTypes, ApiRoutes } from '@/shared/api/types' import { apiErrorIdentify } from '@/shared/api/apiErrorIdentify' import { ShopNewsData } from '../types/types' +import { ACTION_GET_SHOP_NEWS } from '@/shared/constants/constants' // export const getStoreReviews = createAsyncThunk>( export const getShopNews = createAsyncThunk>( //void1- выходные данные, void2- входные данные , thunkConfig- тип store - 'shop-news', // action type, первый аргумент + ACTION_GET_SHOP_NEWS, // action type, первый аргумент async (_, thunkAPI) => { // второй аргумент- асинхронная функция , кот вызовет dispatch в компоненте const { rejectWithValue, extra } = thunkAPI diff --git a/src/widgets/NewsBlock/model/slice/shopNewsSlice.ts b/src/widgets/NewsBlock/model/slice/shopNewsSlice.ts index 02751137..d5969d09 100644 --- a/src/widgets/NewsBlock/model/slice/shopNewsSlice.ts +++ b/src/widgets/NewsBlock/model/slice/shopNewsSlice.ts @@ -1,6 +1,7 @@ import { createSlice } from '@reduxjs/toolkit' import { getShopNews } from '../services/getShopNews' import { ShopNewsSchema } from '../types/types' +import { REDUCER_SHOP_NEWS } from '@/shared/constants/constants' const initialState: ShopNewsSchema = { isLoading: false, @@ -8,7 +9,7 @@ const initialState: ShopNewsSchema = { } export const shopNewsSlice = createSlice({ - name: 'shopNews', + name: REDUCER_SHOP_NEWS, initialState, reducers: {}, extraReducers: builder => { From 4e9e0d9e876c03ac7d38b6ec4c817d1a8725431b Mon Sep 17 00:00:00 2001 From: Yulia Avramenko Date: Mon, 5 Feb 2024 14:30:52 +0300 Subject: [PATCH 2/2] #140-bug-fix --- .../model/selectors/{selectiors.ts => selectors.ts} | 1 - src/widgets/BlogBlock/model/slice/blogPostsSlice.ts | 10 ++++++++-- src/widgets/BlogBlock/model/types/types.ts | 1 + src/widgets/BlogBlock/ui/BlogBlock.tsx | 2 +- src/widgets/NewsBlock/model/selectors/selectors.ts | 1 - 5 files changed, 10 insertions(+), 5 deletions(-) rename src/widgets/BlogBlock/model/selectors/{selectiors.ts => selectors.ts} (71%) diff --git a/src/widgets/BlogBlock/model/selectors/selectiors.ts b/src/widgets/BlogBlock/model/selectors/selectors.ts similarity index 71% rename from src/widgets/BlogBlock/model/selectors/selectiors.ts rename to src/widgets/BlogBlock/model/selectors/selectors.ts index 20da3eba..fd018a25 100644 --- a/src/widgets/BlogBlock/model/selectors/selectiors.ts +++ b/src/widgets/BlogBlock/model/selectors/selectors.ts @@ -1,6 +1,5 @@ import { StateSchema } from '@/app/providers/StoreProvider' export const getBlogPostsSelector = (state: StateSchema) => { - //console.log('Selector BlogPosts', state.blogPosts.posts) return state.blogPosts.posts } diff --git a/src/widgets/BlogBlock/model/slice/blogPostsSlice.ts b/src/widgets/BlogBlock/model/slice/blogPostsSlice.ts index c60a5848..954ead65 100644 --- a/src/widgets/BlogBlock/model/slice/blogPostsSlice.ts +++ b/src/widgets/BlogBlock/model/slice/blogPostsSlice.ts @@ -2,6 +2,7 @@ import { createSlice } from '@reduxjs/toolkit' import { IBlogPostsSchema } from '../types/types' import { getBlogPosts } from '../services/getBlogPosts' import { REDUCER_BLOG_POSTS } from '@/shared/constants/constants' +import { rejectedPayloadHandle } from '@/shared/api/rejectedPayloadHandle' const initialState: IBlogPostsSchema = { isLoading: false, @@ -11,7 +12,11 @@ const initialState: IBlogPostsSchema = { export const blogPostsSlice = createSlice({ name: REDUCER_BLOG_POSTS, initialState, - reducers: {}, + reducers: { + errorReset: state => { + state.error = undefined + } + }, extraReducers: builder => { builder .addCase(getBlogPosts.pending, state => { @@ -21,8 +26,9 @@ export const blogPostsSlice = createSlice({ state.isLoading = false state.posts = payload }) - .addCase(getBlogPosts.rejected, state => { + .addCase(getBlogPosts.rejected, (state, { payload }) => { state.isLoading = false + state.error = rejectedPayloadHandle(payload) }) } }) diff --git a/src/widgets/BlogBlock/model/types/types.ts b/src/widgets/BlogBlock/model/types/types.ts index baf4e95a..54cd5c9c 100644 --- a/src/widgets/BlogBlock/model/types/types.ts +++ b/src/widgets/BlogBlock/model/types/types.ts @@ -29,4 +29,5 @@ export interface IBlogPostData { export interface IBlogPostsSchema { isLoading: boolean posts: IBlogPostData[] + error?: string | string[] } diff --git a/src/widgets/BlogBlock/ui/BlogBlock.tsx b/src/widgets/BlogBlock/ui/BlogBlock.tsx index 63d4c6d6..238f1e42 100644 --- a/src/widgets/BlogBlock/ui/BlogBlock.tsx +++ b/src/widgets/BlogBlock/ui/BlogBlock.tsx @@ -1,7 +1,7 @@ import { FC, useEffect } from 'react' import { getBlogPosts } from '../model/services/getBlogPosts' import { useAppDispatch } from '@/shared/libs/hooks/store' -import { getBlogPostsSelector } from '../model/selectors/selectiors' +import { getBlogPostsSelector } from '../model/selectors/selectors' import { useSelector } from 'react-redux' import IconLink from '@/assets/icons/IconLink' import Heading, { HeadingType } from '@/shared/ui/Heading/Heading' diff --git a/src/widgets/NewsBlock/model/selectors/selectors.ts b/src/widgets/NewsBlock/model/selectors/selectors.ts index c7bc5c60..bcd58e38 100644 --- a/src/widgets/NewsBlock/model/selectors/selectors.ts +++ b/src/widgets/NewsBlock/model/selectors/selectors.ts @@ -1,6 +1,5 @@ import { StateSchema } from '@/app/providers/StoreProvider' export const getShopNewsSelector = (state: StateSchema) => { - // console.log('Selector ShopNews', state.shopNews.news) return state.shopNews.news }