Skip to content

Commit

Permalink
feat: add rating component and ratingCard entity
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoVan committed Mar 7, 2024
1 parent 7d02946 commit 16f92a5
Show file tree
Hide file tree
Showing 20 changed files with 271 additions and 16 deletions.
2 changes: 1 addition & 1 deletion src/entities/Article/ui/ArticleDetails/ArticleDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useAppDispatch } from '@/shared/lib/hook/useAppDispatch/useAppDispatch'
import {
Text, TextAlign, TextSize, TextTheme,
} from '@/shared/ui/Text/Text';
import { Skeleton } from '@/shared/ui/Skeleton/ui/Skeleton';
import { Skeleton } from '@/shared/ui/Skeleton/Skeleton';
import { Avatar } from '@/shared/ui/Avatar/Avatar';
import EyeIcon from '@/shared/assets/icons/eye-20-20.svg';
import CalendarIcon from '@/shared/assets/icons/calendar-20-20.svg';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { memo } from 'react';
import { classNames } from '@/shared/lib/classNames/classNames';
import { Card } from '@/shared/ui/Card/Card';
import { Skeleton } from '@/shared/ui/Skeleton/ui/Skeleton';
import { Skeleton } from '@/shared/ui/Skeleton/Skeleton';
import { ArticleView } from '../../model/consts/consts';
import cls from './ArticleListItem.module.scss';

Expand Down
2 changes: 1 addition & 1 deletion src/entities/Comment/ui/CommentCard/CommentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import cls from './CommentCard.module.scss';
import { Comment } from '../../types/comment';
import { Avatar } from '../../../../shared/ui/Avatar/Avatar';
import { Text } from '../../../../shared/ui/Text/Text';
import { Skeleton } from '../../../../shared/ui/Skeleton/ui/Skeleton';
import { Skeleton } from '../../../../shared/ui/Skeleton/Skeleton';

interface CommentCardProps {
className?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { memo } from 'react';
import { classNames } from '@/shared/lib/classNames/classNames';
import { VStack } from '@/shared/ui/Stack';
import { Skeleton } from '@/shared/ui/Skeleton/ui/Skeleton';
import { Skeleton } from '@/shared/ui/Skeleton/Skeleton';
import { NotificationItem } from '../../ui/NotificationItem/NotificationItem';
import { useNotifications } from '../../api/notificationAPI';
import cls from './NotificationList.module.scss';
Expand Down
1 change: 1 addition & 0 deletions src/entities/Rating/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RatingCard } from './ui/RatingCard';
17 changes: 17 additions & 0 deletions src/entities/Rating/ui/RatingCard.stories.tsx
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 { RatingCard } from './RatingCard';

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

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

export const Normal = Template.bind({});
Normal.args = {};
102 changes: 102 additions & 0 deletions src/entities/Rating/ui/RatingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { useTranslation } from 'react-i18next';
import { memo, useCallback, useState } from 'react';
import { BrowserView, MobileView } from 'react-device-detect';
import { classNames } from '@/shared/lib/classNames/classNames';
import { Card } from '@/shared/ui/Card/Card';
import { HStack, VStack } from '@/shared/ui/Stack';
import { Text } from '@/shared/ui/Text/Text';
import { StarRating } from '@/shared/ui/StarRating/StarRating';
import { Modal } from '@/shared/ui/Modal/Modal';
import { Input } from '@/shared/ui/Input/Input';
import { Button, ButtonSize, ButtonTheme } from '@/shared/ui/Button/Button';
import { Drawer } from '@/shared/ui/Drawer/Drawer';

interface RatingCardProps {
className?: string;
title?: string;
feedbackTitle?: string;
hasFeedback?: boolean;
onCancel?: (starsCount: number) => void;
onAccept?: (starsCount: number, feedback?: string) => void;
}

export const RatingCard = memo((props: RatingCardProps) => {
const {
className,
onAccept,
feedbackTitle,
hasFeedback,
onCancel,
title,
} = props;
const { t } = useTranslation();
const [isModalOpen, setIsModalOpen] = useState(false);
const [starsCount, setStarsCount] = useState(0);
const [feedback, setFeedback] = useState('');

const onSelectStars = useCallback((selectedStarsCount: number) => {
setStarsCount(selectedStarsCount);
if (hasFeedback) {
setIsModalOpen(true);
} else {
onAccept?.(selectedStarsCount);
}
}, [hasFeedback, onAccept]);

const acceptHandle = useCallback(() => {
setIsModalOpen(false);
onAccept?.(starsCount, feedback);
}, [feedback, onAccept, starsCount]);

const cancelHandle = useCallback(() => {
setIsModalOpen(false);
onCancel?.(starsCount);
}, [onCancel, starsCount]);

const modalContent = (
<>
<Text
title={feedbackTitle}
/>
<Input
value={feedback}
onChange={setFeedback}
placeholder={t('Ваш отзыв')}
/>
</>
);

return (
<Card className={classNames('', {}, [className])}>
<VStack align="center" gap="8">
<Text title={title} />
<StarRating size={40} onSelect={onSelectStars} />
</VStack>
<BrowserView>
<Modal isOpen={isModalOpen} lazy>
<VStack max gap="32">
{modalContent}
<HStack max gap="16" justify="between">
<Button onClick={cancelHandle} theme={ButtonTheme.OUTLINE_RED}>
{t('Закрыть')}
</Button>
<Button onClick={acceptHandle}>
{t('Отправить')}
</Button>
</HStack>
</VStack>
</Modal>
</BrowserView>
<MobileView>
<Drawer isOpen={isModalOpen} lazy onClose={cancelHandle}>
<VStack gap="32">
{modalContent}
<Button fullWidth onClick={acceptHandle} size={ButtonSize.L}>
{t('Отправить')}
</Button>
</VStack>
</Drawer>
</MobileView>
</Card>
);
});
2 changes: 2 additions & 0 deletions src/pages/MainPage/ui/MainPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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
3 changes: 3 additions & 0 deletions src/shared/assets/icons/star.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/shared/ui/Button/Button.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,7 @@
.isDisabled {
opacity: 0.5;
}

.fullWidth {
width: 100%;
}
3 changes: 3 additions & 0 deletions src/shared/ui/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>{
size?: ButtonSize;
isDisabled?: boolean;
children?: ReactNode;
fullWidth?: boolean;
}

export const Button = memo((props: ButtonProps) => {
Expand All @@ -36,12 +37,14 @@ export const Button = memo((props: ButtonProps) => {
square,
isDisabled,
size = ButtonSize.M,
fullWidth,
...otherProps
} = props;

const mods:Mods = {
[cls.square]: square,
[cls.isDisabled]: isDisabled,
[cls.fullWidth]: fullWidth,
};

const additional = [
Expand Down
20 changes: 12 additions & 8 deletions src/shared/ui/Icon/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { useTranslation } from 'react-i18next';
import React, { memo } from 'react';
import { classNames } from '@/shared/lib/classNames/classNames';
import cls from './Icon.module.scss';

interface IconProps {
className?: string;
Svg: React.VFC<React.SVGProps<SVGSVGElement>>;
inverted?: boolean;
interface IconProps extends React.SVGProps<SVGSVGElement> {
className?: string;
Svg: React.VFC<React.SVGProps<SVGSVGElement>>;
inverted?: boolean;
}

export const Icon = memo(({ className, Svg, inverted }: IconProps) => {
const { t } = useTranslation();
export const Icon = memo((props: IconProps) => {
const {
className, Svg, inverted, ...otherProps
} = props;
return (
<Svg className={classNames(inverted ? cls.inverted : cls.Icon, {}, [className])} />
<Svg
className={classNames(inverted ? cls.inverted : cls.Icon, {}, [className])}
{...otherProps}
/>
);
});
15 changes: 12 additions & 3 deletions src/shared/ui/Popups/ui/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function Dropdown(props: DropdownProps) {
{trigger}
</Menu.Button>
<Menu.Items className={classNames(cls.menu, {}, menuClasses)}>
{items.map((item) => {
{items.map((item, index) => {
const content = ({ active }: {active: boolean}) => (
<button
type="button"
Expand All @@ -51,14 +51,23 @@ export function Dropdown(props: DropdownProps) {

if (item.href) {
return (
<Menu.Item as={AppLink} to={item.href} disabled={item.disabled}>
<Menu.Item
as={AppLink}
to={item.href}
disabled={item.disabled}
key={`dropdown-key-${index}`}
>
{content}
</Menu.Item>
);
}

return (
<Menu.Item as={Fragment} disabled={item.disabled}>
<Menu.Item
as={Fragment}
disabled={item.disabled}
key={`dropdown-key-${index}`}
>
{content}
</Menu.Item>
);
Expand Down
7 changes: 6 additions & 1 deletion src/shared/ui/Popups/ui/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ export const Popover = memo((props: PopoverProps) => {
<HPopover
className={classNames(cls.Popover, {}, [className, popupCls.popup])}
>
<HPopover.Button className={popupCls.trigger}>{trigger}</HPopover.Button>
<HPopover.Button
as="div"
className={popupCls.trigger}
>
{trigger}
</HPopover.Button>

<HPopover.Panel className={classNames(cls.panel, {}, menuClasses)}>
{children}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions src/shared/ui/StarRating/StarRating.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.StarIcon {
cursor: pointer;
}

.normal {
fill: none;
}

.hovered {
fill: var(--primary-color);
}

.isSelected {
cursor: auto;
}
26 changes: 26 additions & 0 deletions src/shared/ui/StarRating/StarRating.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { StarRating } from './StarRating';

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

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

export const Default = Template.bind({});
Default.args = {
onSelect: (starCount: number) => {
console.log(`Selected stars: ${starCount}`);
},
};

export const WithSelectedStars = Template.bind({});
WithSelectedStars.args = {
selectedStars: 3,
};
64 changes: 64 additions & 0 deletions src/shared/ui/StarRating/StarRating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { memo, useState } from 'react';
import { classNames } from '@/shared/lib/classNames/classNames';
import cls from './StarRating.module.scss';
import { Icon } from '@/shared/ui/Icon/Icon';
import StarIcon from '@/shared/assets/icons/star.svg';

interface StarRatingProps {
className?: string;
onSelect?: (starCount: number) => void;
size?: number;
selectedStars?: number;
}

const stars = [1, 2, 3, 4, 5];

export const StarRating = memo((props: StarRatingProps) => {
const {
className,
onSelect,
size = 30,
selectedStars = 0,
} = props;

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

const onHover = (starsCount: number) => {
if (!isSelected) {
setCurrentStarsCount(starsCount);
}
};

const onLeave = () => {
if (!isSelected) {
setCurrentStarsCount(0);
}
};

const onClick = (starsCount: number) => {
if (!isSelected) {
onSelect?.(starsCount);
setCurrentStarsCount(starsCount);
setIsSelected(true);
}
};

return (
<div className={classNames(cls.StarRating, {}, [className])}>
{stars.map((starNumber) => (
<Icon
className={classNames(cls.StarIcon, { [cls.selected]: isSelected }, [currentStarsCount >= starNumber ? cls.hovered : cls.normal])}
Svg={StarIcon}
key={starNumber}
width={size}
height={size}
onMouseLeave={onLeave}
onMouseEnter={() => onHover(starNumber)}
onClick={() => onClick(starNumber)}

/>
))}
</div>
);
});

0 comments on commit 16f92a5

Please sign in to comment.