Commit 602755e4 authored by tom's avatar tom

add rating component

parent 8a0c8788
......@@ -41,6 +41,7 @@ const RESTRICTED_MODULES = {
'Heading', 'Badge', 'Tabs', 'Show', 'Hide', 'Checkbox', 'CheckboxGroup',
'Table', 'TableRoot', 'TableBody', 'TableHeader', 'TableRow', 'TableCell',
'Menu', 'MenuRoot', 'MenuTrigger', 'MenuContent', 'MenuItem', 'MenuTriggerItem', 'MenuRadioItemGroup', 'MenuContextTrigger',
'Rating', 'RatingGroup',
],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
},
......
import { RatingGroup, useRatingGroup } from '@chakra-ui/react';
import * as React from 'react';
import IconSvg from 'ui/shared/IconSvg';
export interface RatingProps extends Omit<RatingGroup.RootProviderProps, 'value'> {
count?: number;
label?: string | Array<string>;
defaultValue?: number;
onValueChange?: ({ value }: { value: number }) => void;
readOnly?: boolean;
}
export const Rating = React.forwardRef<HTMLDivElement, RatingProps>(
function Rating(props, ref) {
const { count = 5, label: labelProp, defaultValue, onValueChange, readOnly, ...rest } = props;
const store = useRatingGroup({ count, defaultValue, onValueChange, readOnly });
const highlightedIndex = store.hovering && !readOnly ? store.hoveredValue : store.value;
const label = Array.isArray(labelProp) ? labelProp[highlightedIndex - 1] : labelProp;
return (
<RatingGroup.RootProvider ref={ ref } value={ store } { ...rest }>
<RatingGroup.HiddenInput/>
<RatingGroup.Control>
{ Array.from({ length: count }).map((_, index) => {
const icon = index < highlightedIndex ? <IconSvg name="star_filled"/> : <IconSvg name="star_outline"/>;
return (
<RatingGroup.Item key={ index } index={ index + 1 }>
<RatingGroup.ItemIndicator icon={ icon }/>
</RatingGroup.Item>
);
}) }
</RatingGroup.Control>
{ label && <RatingGroup.Label>{ label }</RatingGroup.Label> }
</RatingGroup.RootProvider>
);
},
);
......@@ -404,6 +404,10 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
down: { value: { _light: '{colors.red.600}', _dark: '{colors.red.400}' } },
},
},
rating: {
DEFAULT: { value: { _light: '{colors.gray.200}', _dark: '{colors.gray.700}' } },
highlighted: { value: '{colors.yellow.400}' },
},
heading: {
DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
},
......
......@@ -18,6 +18,7 @@ import { recipe as popover } from './popover.recipe';
import { recipe as progressCircle } from './progress-circle.recipe';
import { recipe as radioGroup } from './radio-group.recipe';
import { recipe as radiomark } from './radiomark.recipe';
import { recipe as ratingGroup } from './rating-group.recipe';
import { recipe as select } from './select.recipe';
import { recipe as skeleton } from './skeleton.recipe';
import { recipe as spinner } from './spinner.recipe';
......@@ -57,6 +58,7 @@ export const slotRecipes = {
popover,
progressCircle,
radioGroup,
ratingGroup,
select,
stat,
'switch': switchRecipe,
......
import { defineSlotRecipe } from '@chakra-ui/react';
export const recipe = defineSlotRecipe({
className: 'chakra-rating-group',
slots: [ 'root', 'control', 'item', 'itemIndicator' ],
base: {
root: {
display: 'inline-flex',
alignItems: 'center',
columnGap: 3,
},
control: {
display: 'inline-flex',
alignItems: 'center',
gap: 1,
},
item: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
cursor: 'pointer',
_icon: {
width: '100%',
height: '100%',
display: 'inline-block',
flexShrink: 0,
position: 'absolute',
left: 0,
top: 0,
},
},
itemIndicator: {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
_icon: {
stroke: 'none',
width: '100%',
height: '100%',
display: 'inline-block',
flexShrink: 0,
position: 'absolute',
left: 0,
top: 0,
},
'& [data-bg]': {
color: 'rating',
},
'& [data-fg]': {
color: 'transparent',
},
'&[data-highlighted]:not([data-half])': {
'& [data-fg]': {
color: 'rating.highlighted',
},
},
'&[data-half]': {
'& [data-fg]': {
color: 'rating.highlighted',
clipPath: 'inset(0 50% 0 0)',
},
},
},
},
variants: {
size: {
md: {
item: {
boxSize: 5,
},
root: {
textStyle: 'md',
},
},
},
},
defaultVariants: {
size: 'md',
},
});
......@@ -4,9 +4,9 @@ import React from 'react';
import type { AppRating } from 'types/client/marketplace';
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
import { Rating } from 'toolkit/chakra/rating';
import IconSvg from 'ui/shared/IconSvg';
import Stars from './Stars';
import type { RateFunction } from './useRatings';
const ratingDescriptions = [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ];
......@@ -21,25 +21,8 @@ type Props = {
};
const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: Props) => {
const [ hovered, setHovered ] = React.useState(-1);
const filledIndex = React.useMemo(() => {
if (hovered >= 0) {
return hovered;
}
return userRating?.value ? userRating?.value - 1 : -1;
}, [ userRating, hovered ]);
const handleMouseOverFactory = React.useCallback((index: number) => () => {
setHovered(index);
}, []);
const handleMouseOut = React.useCallback(() => {
setHovered(-1);
}, []);
const handleRateFactory = React.useCallback((index: number) => () => {
rate(appId, rating?.recordId, userRating?.recordId, index + 1, source);
const handleValueChange = React.useCallback(({ value }: { value: number }) => {
rate(appId, rating?.recordId, userRating?.recordId, value, source);
}, [ appId, rating, rate, userRating, source ]);
if (isSending) {
......@@ -61,19 +44,12 @@ const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }:
{ userRating ? 'App is already rated by you' : 'How was your experience?' }
</Text>
</Flex>
<Flex alignItems="center" h="32px">
<Stars
filledIndex={ filledIndex }
onMouseOverFactory={ handleMouseOverFactory }
onMouseOut={ handleMouseOut }
onClickFactory={ handleRateFactory }
/>
{ (filledIndex >= 0) && (
<Text fontSize="md" ml={ 3 }>
{ ratingDescriptions[filledIndex] }
</Text>
) }
</Flex>
<Rating
defaultValue={ userRating?.value }
onValueChange={ handleValueChange }
label={ ratingDescriptions }
mt={ 1 }
/>
</>
);
};
......
......@@ -6,10 +6,10 @@ import type { AppRating } from 'types/client/marketplace';
import config from 'configs/app';
import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
import { Rating } from 'toolkit/chakra/rating';
import { Skeleton } from 'toolkit/chakra/skeleton';
import Content from './PopoverContent';
import Stars from './Stars';
import TriggerButton from './TriggerButton';
import type { RateFunction } from './useRatings';
......@@ -28,7 +28,7 @@ type Props = {
source: EventPayload<EventTypes.APP_FEEDBACK>['Source'];
};
const Rating = ({
const MarketplaceRating = ({
appId, rating, userRating, rate,
isSending, isLoading, fullView, canRate, source,
}: Props) => {
......@@ -46,7 +46,7 @@ const Rating = ({
>
{ fullView && (
<>
<Stars filledIndex={ (rating?.value || 0) - 1 }/>
<Rating defaultValue={ Math.floor(rating?.value || 0) } readOnly key={ rating?.value }/>
<Text fontSize="md" ml={ 2 }>{ rating?.value }</Text>
{ rating?.count && <Text color="text.secondary" textStyle="md" ml={ 1 }>({ rating?.count })</Text> }
</>
......@@ -78,4 +78,4 @@ const Rating = ({
);
};
export default Rating;
export default MarketplaceRating;
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { MouseEventHandler } from 'react';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
filledIndex: number;
onMouseOverFactory?: (index: number) => MouseEventHandler<HTMLDivElement>;
onMouseOut?: () => void;
onClickFactory?: (index: number) => MouseEventHandler<HTMLDivElement>;
};
const Stars = ({ filledIndex, onMouseOverFactory, onMouseOut, onClickFactory }: Props) => {
const outlineStartColor = onMouseOverFactory ? 'gray.400' : { _light: 'gray.200', _dark: 'gray.700' };
return (
<Flex>
{ Array(5).fill(null).map((_, index) => (
<IconSvg
key={ index }
name={ filledIndex >= index ? 'star_filled' : 'star_outline' }
color={ filledIndex >= index ? 'yellow.400' : outlineStartColor }
w={ 6 } // 5 + 1 padding
h={ 5 }
pr={ 1 } // use padding intead of margin so that there are no empty spaces between stars without hover effect
_last={{ w: 5, pr: 0 }}
cursor={ onMouseOverFactory ? 'pointer' : 'default' }
onMouseOver={ onMouseOverFactory?.(index) }
onMouseOut={ onMouseOut }
onClick={ onClickFactory?.(index) }
/>
)) }
</Flex>
);
};
export default Stars;
......@@ -24,6 +24,7 @@ import PinInputShowcase from 'ui/showcases/PinInput';
import PopoverShowcase from 'ui/showcases/Popover';
import ProgressCircleShowcase from 'ui/showcases/ProgressCircle';
import RadioShowcase from 'ui/showcases/Radio';
import RatingShowcase from 'ui/showcases/Rating';
import SelectShowcase from 'ui/showcases/Select';
import SkeletonShowcase from 'ui/showcases/Skeleton';
import SpinnerShowcase from 'ui/showcases/Spinner';
......@@ -61,6 +62,7 @@ const tabs = [
{ label: 'Pagination', value: 'pagination', component: <PaginationShowcase/> },
{ label: 'Progress Circle', value: 'progress-circle', component: <ProgressCircleShowcase/> },
{ label: 'Radio', value: 'radio', component: <RadioShowcase/> },
{ label: 'Rating', value: 'rating', component: <RatingShowcase/> },
{ label: 'Pin input', value: 'pin-input', component: <PinInputShowcase/> },
{ label: 'Popover', value: 'popover', component: <PopoverShowcase/> },
{ label: 'Select', value: 'select', component: <SelectShowcase/> },
......
......@@ -24,7 +24,6 @@ interface Props {
showUpdateMetadataItem?: boolean;
}
// TODO @tom2drum fix modal open on menu item click
const AccountActionsMenu = ({ isLoading, className, showUpdateMetadataItem }: Props) => {
const router = useRouter();
......
import React from 'react';
import { Rating } from 'toolkit/chakra/rating';
import { Section, Container, SectionHeader, SamplesStack, Sample } from './parts';
const RatingShowcase = () => {
return (
<Container value="rating">
<Section>
<SectionHeader>Size</SectionHeader>
<SamplesStack>
<Sample label="size: md">
<Rating defaultValue={ 3 } label={ [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ] }/>
</Sample>
</SamplesStack>
</Section>
<Section>
<SectionHeader>Read-only</SectionHeader>
<SamplesStack>
<Sample label="readOnly: true">
<Rating defaultValue={ 3 } label={ [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ] } readOnly/>
</Sample>
</SamplesStack>
</Section>
</Container>
);
};
export default React.memo(RatingShowcase);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment