Skip to content

Commit

Permalink
feat: add article rating feature, add rtk mutation post request
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoVan committed Mar 8, 2024
1 parent 16f92a5 commit e5d0a61
Show file tree
Hide file tree
Showing 16 changed files with 236 additions and 14 deletions.
3 changes: 3 additions & 0 deletions extractedTranslations/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@
"read more": "",
"throw error": "throw error",
"views": "",
"Ваш отзыв": "",
"Главная страница": "",
"Закрыть": "",
"Обновить страницу": "",
"Отправить": "",
"Произошла непредвиденная ошибка": "",
"Статьи не найдены": ""
}
76 changes: 74 additions & 2 deletions json-server/db.json
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,78 @@
"username": "ulbi tv",
"avatar": "https://xakep.ru/wp-content/uploads/2018/05/171485/KuroiSH-hacker.jpg"
}
],
"article-ratings": [
{
"id": "1",
"rate": 4,
"feedback": "Хорошая статья",
"userId": "1",
"articleId": "1"
},
{
"id": "2",
"rate": 4,
"feedback": "Хорошая статья",
"userId": "1",
"articleId": "2"
},
{
"userId": "1",
"articleId": "3",
"rate": 3,
"feedback": "",
"id": "I7ofk1u"
},
{
"userId": "1",
"articleId": "9",
"rate": 5,
"feedback": "asdasdasdasd",
"id": "Y7MhPzo"
},
{
"userId": "1",
"articleId": "10",
"rate": 1,
"feedback": "отстой",
"id": "II0a75O"
},
{
"userId": "1",
"articleId": "7",
"rate": 3,
"feedback": "",
"id": "bcPnbbZ"
},
{
"userId": "1",
"articleId": "4",
"rate": 3,
"id": "Jh9k67H"
},
{
"userId": "1",
"articleId": "5",
"rate": 4,
"feedback": "123",
"id": "QZWoJpm"
}
],
"profile-ratings": [
{
"id": "1",
"rate": 4,
"feedback": "Хорошая статья",
"userId": "1",
"profileId": "1"
},
{
"id": "2",
"rate": 4,
"feedback": "Хорошая статья",
"userId": "1",
"profileId": "2"
}
]
}

}
1 change: 1 addition & 0 deletions src/entities/Rating/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { RatingCard } from './ui/RatingCard';
export type { Rating } from './model/types/types';
4 changes: 4 additions & 0 deletions src/entities/Rating/model/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Rating {
rate: number;
feedback?: string;
}
12 changes: 7 additions & 5 deletions src/entities/Rating/ui/RatingCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ interface RatingCardProps {
hasFeedback?: boolean;
onCancel?: (starsCount: number) => void;
onAccept?: (starsCount: number, feedback?: string) => void;
rate?: number;
}

export const RatingCard = memo((props: RatingCardProps) => {
Expand All @@ -28,10 +29,11 @@ export const RatingCard = memo((props: RatingCardProps) => {
hasFeedback,
onCancel,
title,
rate = 0,
} = props;
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [starsCount, setStarsCount] = useState(0);
const [starsCount, setStarsCount] = useState(rate);
const [feedback, setFeedback] = useState('');

const onSelectStars = useCallback((selectedStarsCount: number) => {
Expand Down Expand Up @@ -67,10 +69,10 @@ export const RatingCard = memo((props: RatingCardProps) => {
);

return (
<Card className={classNames('', {}, [className])}>
<VStack align="center" gap="8">
<Text title={title} />
<StarRating size={40} onSelect={onSelectStars} />
<Card max className={classNames('', {}, [className])}>
<VStack align="center" gap="8" max>
<Text title={starsCount ? t('Thanks for review!') : title} />
<StarRating selectedStars={starsCount} size={40} onSelect={onSelectStars} />
</VStack>
<BrowserView>
<Modal isOpen={isModalOpen} lazy>
Expand Down
39 changes: 39 additions & 0 deletions src/features/articleRating/api/articleRatingAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { rtkApi } from '@/shared/api/rtkApi';
import { Rating } from '@/entities/Rating';

interface GetArticleRatingArg {
userId: string
articleId: string
}

interface RateArticleArg {
userId: string
articleId: string
rate: number;
feedback?: string
}

const articleRatingAPI = rtkApi.injectEndpoints({
endpoints: (build) => ({
getArticleRating: build.query<Rating[], GetArticleRatingArg>({
query: ({ userId, articleId }) => ({
url: '/article-ratings',
params: {
userId,
articleId,
},

}),
}),
rateArticle: build.mutation<void, RateArticleArg>({
query: (arg) => ({
url: '/article-ratings',
method: 'POST',
body: arg,
}),
}),
}),
});

export const useGetArticleRating = articleRatingAPI.useGetArticleRatingQuery;
export const useRateArticle = articleRatingAPI.useRateArticleMutation;
1 change: 1 addition & 0 deletions src/features/articleRating/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ArticleRatingAsync as ArticleRating } from './ui/ArticleRating/ArticleRating.async';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { lazy, Suspense } from 'react';
import { ArticleRatingProps } from './ArticleRating';
import { Skeleton } from '@/shared/ui/Skeleton/Skeleton';

const ArticleRatingLazy = lazy(() => import('./ArticleRating'));

export const ArticleRatingAsync = (props: ArticleRatingProps) => (
<Suspense fallback={<Skeleton width="100%" height={140} />}>
<ArticleRatingLazy {...props} />
</Suspense>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import ArticleRating from './ArticleRating';

export default {
title: 'shared/ArticleRating',
component: ArticleRating,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof ArticleRating>;

const Template: ComponentStory<typeof ArticleRating> = (args) => <ArticleRating {...args} />;

export const Normal = Template.bind({});
Normal.args = {};
65 changes: 65 additions & 0 deletions src/features/articleRating/ui/ArticleRating/ArticleRating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useTranslation } from 'react-i18next';
import { memo, useCallback } from 'react';
import { useSelector } from 'react-redux';
import { RatingCard } from '@/entities/Rating';
import { useGetArticleRating, useRateArticle } from '../../api/articleRatingAPI';
import { getUserAuthData } from '@/entities/User';
import { Skeleton } from '@/shared/ui/Skeleton/Skeleton';

export interface ArticleRatingProps {
className?: string;
articleId: string;
}

const ArticleRating = memo((props: ArticleRatingProps) => {
const { className, articleId } = props;
const { t } = useTranslation();
const userData = useSelector(getUserAuthData);
const { data, isLoading } = useGetArticleRating({
articleId,
userId: userData?.id ?? '',
});

const [rateArticleMutation] = useRateArticle();

const rating = data?.[0];

const handleRateArticle = useCallback((starsCount: number, feedback?: string) => {
try {
rateArticleMutation({
userId: userData?.id ?? '',
articleId,
rate: starsCount,
feedback,
});
} catch (e) {
console.log(e);
}
}, [articleId, rateArticleMutation, userData?.id]);

const onCancel = useCallback((starsCount: number) => {
handleRateArticle(starsCount);
}, [handleRateArticle]);

const onAccept = useCallback((starsCount: number, feedback?: string) => {
handleRateArticle(starsCount, feedback);
}, [handleRateArticle]);

if (isLoading) {
return <Skeleton width="100%" height={120} border="8px" />;
}

return (
<RatingCard
className={className}
title={t('Rate the article')}
feedbackTitle={t('Leave your feedback on the article, it will help improve the quality')}
hasFeedback
rate={rating?.rate}
onAccept={onAccept}
onCancel={onCancel}
/>
);
});

export default ArticleRating;
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useTranslation } from 'react-i18next';
import { memo } from 'react';
import { useParams } from 'react-router-dom';
import {
Expand All @@ -13,21 +12,26 @@ import { ArticleDetailsComments } from '../../ui/ArticleDetailsComments/ArticleD
import { ArticlesDetailsPageHeader } from '../../ui/ArticlesDetailsPageHeader/ArticlesDetailsPageHeader';
import { articleDetailsPageReducer } from '../../model/slices';
import { ArticleDetails } from '../../../../entities/Article';
import { ArticleRating } from '@/features/articleRating';

const reducers: ReducersList = {
articleDetailsPage: articleDetailsPageReducer,
};

const ArticlesDetailsPage = (props: any) => {
const { t } = useTranslation();
const { id } = useParams<{ id: string }>();

if (!id) {
return null;
}

return (
<DynamicModuleLoader reducers={reducers}>
<Page>
<VStack gap="16" max>
<ArticlesDetailsPageHeader />
<ArticleDetails id={id} />
<ArticleRating articleId={id} />
<ArticleRecommendationsList />
<ArticleDetailsComments id={id} />
</VStack>
Expand Down
2 changes: 0 additions & 2 deletions src/pages/MainPage/ui/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import React, { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { RatingCard } from '@/entities/Rating';

const MainPage = memo((props: any) => {
const { t } = useTranslation();

return (
<div>
{t('Главная страница')}
<RatingCard title="123" feedbackTitle="heyyyy" hasFeedback />
</div>
);
});
Expand Down
1 change: 0 additions & 1 deletion src/shared/api/rtkApi.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import { USER_LOCALSTORAGE_KEY } from '@/shared/const/localstorage';

// Define a service using a base URL and expected endpoints
export const rtkApi = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
Expand Down
4 changes: 4 additions & 0 deletions src/shared/ui/Card/Card.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@
.outlined {
border: 1px solid var(--primary-color);
}

.max {
width: 100%;
}
4 changes: 3 additions & 1 deletion src/shared/ui/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface CardProps extends HTMLAttributes<HTMLDivElement> {
className?: string;
children: ReactNode;
theme?: CardTheme;
max?: boolean;

}

Expand All @@ -19,11 +20,12 @@ export const Card = memo((props: CardProps) => {
className,
children,
theme = CardTheme.NORMAL,
max,
...otherProps
} = props;
return (
<div
className={classNames(cls.Card, {}, [className, cls[theme]])}
className={classNames(cls.Card, { [cls.max]: max }, [className, cls[theme]])}
{...otherProps}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/ui/StarRating/StarRating.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const StarRating = memo((props: StarRatingProps) => {
selectedStars = 0,
} = props;

const [currentStarsCount, setCurrentStarsCount] = useState(0);
const [currentStarsCount, setCurrentStarsCount] = useState(selectedStars);
const [isSelected, setIsSelected] = useState(Boolean(selectedStars));

const onHover = (starsCount: number) => {
Expand Down

0 comments on commit e5d0a61

Please sign in to comment.