Commit 602755e4 authored by tom's avatar tom

add rating component

parent 8a0c8788
...@@ -41,6 +41,7 @@ const RESTRICTED_MODULES = { ...@@ -41,6 +41,7 @@ const RESTRICTED_MODULES = {
'Heading', 'Badge', 'Tabs', 'Show', 'Hide', 'Checkbox', 'CheckboxGroup', 'Heading', 'Badge', 'Tabs', 'Show', 'Hide', 'Checkbox', 'CheckboxGroup',
'Table', 'TableRoot', 'TableBody', 'TableHeader', 'TableRow', 'TableCell', 'Table', 'TableRoot', 'TableBody', 'TableHeader', 'TableRow', 'TableCell',
'Menu', 'MenuRoot', 'MenuTrigger', 'MenuContent', 'MenuItem', 'MenuTriggerItem', 'MenuRadioItemGroup', 'MenuContextTrigger', 'Menu', 'MenuRoot', 'MenuTrigger', 'MenuContent', 'MenuItem', 'MenuTriggerItem', 'MenuRadioItemGroup', 'MenuContextTrigger',
'Rating', 'RatingGroup',
], ],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead', 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'] = { ...@@ -404,6 +404,10 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
down: { value: { _light: '{colors.red.600}', _dark: '{colors.red.400}' } }, 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: { heading: {
DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } }, DEFAULT: { value: { _light: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
}, },
......
...@@ -18,6 +18,7 @@ import { recipe as popover } from './popover.recipe'; ...@@ -18,6 +18,7 @@ import { recipe as popover } from './popover.recipe';
import { recipe as progressCircle } from './progress-circle.recipe'; import { recipe as progressCircle } from './progress-circle.recipe';
import { recipe as radioGroup } from './radio-group.recipe'; import { recipe as radioGroup } from './radio-group.recipe';
import { recipe as radiomark } from './radiomark.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 select } from './select.recipe';
import { recipe as skeleton } from './skeleton.recipe'; import { recipe as skeleton } from './skeleton.recipe';
import { recipe as spinner } from './spinner.recipe'; import { recipe as spinner } from './spinner.recipe';
...@@ -57,6 +58,7 @@ export const slotRecipes = { ...@@ -57,6 +58,7 @@ export const slotRecipes = {
popover, popover,
progressCircle, progressCircle,
radioGroup, radioGroup,
ratingGroup,
select, select,
stat, stat,
'switch': switchRecipe, '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'; ...@@ -4,9 +4,9 @@ import React from 'react';
import type { AppRating } from 'types/client/marketplace'; import type { AppRating } from 'types/client/marketplace';
import type { EventTypes, EventPayload } from 'lib/mixpanel/index'; import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
import { Rating } from 'toolkit/chakra/rating';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import Stars from './Stars';
import type { RateFunction } from './useRatings'; import type { RateFunction } from './useRatings';
const ratingDescriptions = [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ]; const ratingDescriptions = [ 'Very bad', 'Bad', 'Average', 'Good', 'Excellent' ];
...@@ -21,25 +21,8 @@ type Props = { ...@@ -21,25 +21,8 @@ type Props = {
}; };
const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: Props) => { const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: Props) => {
const [ hovered, setHovered ] = React.useState(-1); const handleValueChange = React.useCallback(({ value }: { value: number }) => {
rate(appId, rating?.recordId, userRating?.recordId, value, source);
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);
}, [ appId, rating, rate, userRating, source ]); }, [ appId, rating, rate, userRating, source ]);
if (isSending) { if (isSending) {
...@@ -61,19 +44,12 @@ const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }: ...@@ -61,19 +44,12 @@ const PopoverContent = ({ appId, rating, userRating, rate, isSending, source }:
{ userRating ? 'App is already rated by you' : 'How was your experience?' } { userRating ? 'App is already rated by you' : 'How was your experience?' }
</Text> </Text>
</Flex> </Flex>
<Flex alignItems="center" h="32px"> <Rating
<Stars defaultValue={ userRating?.value }
filledIndex={ filledIndex } onValueChange={ handleValueChange }
onMouseOverFactory={ handleMouseOverFactory } label={ ratingDescriptions }
onMouseOut={ handleMouseOut } mt={ 1 }
onClickFactory={ handleRateFactory }
/> />
{ (filledIndex >= 0) && (
<Text fontSize="md" ml={ 3 }>
{ ratingDescriptions[filledIndex] }
</Text>
) }
</Flex>
</> </>
); );
}; };
......
...@@ -6,10 +6,10 @@ import type { AppRating } from 'types/client/marketplace'; ...@@ -6,10 +6,10 @@ import type { AppRating } from 'types/client/marketplace';
import config from 'configs/app'; import config from 'configs/app';
import type { EventTypes, EventPayload } from 'lib/mixpanel/index'; import type { EventTypes, EventPayload } from 'lib/mixpanel/index';
import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover'; import { PopoverBody, PopoverContent, PopoverRoot } from 'toolkit/chakra/popover';
import { Rating } from 'toolkit/chakra/rating';
import { Skeleton } from 'toolkit/chakra/skeleton'; import { Skeleton } from 'toolkit/chakra/skeleton';
import Content from './PopoverContent'; import Content from './PopoverContent';
import Stars from './Stars';
import TriggerButton from './TriggerButton'; import TriggerButton from './TriggerButton';
import type { RateFunction } from './useRatings'; import type { RateFunction } from './useRatings';
...@@ -28,7 +28,7 @@ type Props = { ...@@ -28,7 +28,7 @@ type Props = {
source: EventPayload<EventTypes.APP_FEEDBACK>['Source']; source: EventPayload<EventTypes.APP_FEEDBACK>['Source'];
}; };
const Rating = ({ const MarketplaceRating = ({
appId, rating, userRating, rate, appId, rating, userRating, rate,
isSending, isLoading, fullView, canRate, source, isSending, isLoading, fullView, canRate, source,
}: Props) => { }: Props) => {
...@@ -46,7 +46,7 @@ const Rating = ({ ...@@ -46,7 +46,7 @@ const Rating = ({
> >
{ fullView && ( { 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> <Text fontSize="md" ml={ 2 }>{ rating?.value }</Text>
{ rating?.count && <Text color="text.secondary" textStyle="md" ml={ 1 }>({ rating?.count })</Text> } { rating?.count && <Text color="text.secondary" textStyle="md" ml={ 1 }>({ rating?.count })</Text> }
</> </>
...@@ -78,4 +78,4 @@ const Rating = ({ ...@@ -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'; ...@@ -24,6 +24,7 @@ import PinInputShowcase from 'ui/showcases/PinInput';
import PopoverShowcase from 'ui/showcases/Popover'; import PopoverShowcase from 'ui/showcases/Popover';
import ProgressCircleShowcase from 'ui/showcases/ProgressCircle'; import ProgressCircleShowcase from 'ui/showcases/ProgressCircle';
import RadioShowcase from 'ui/showcases/Radio'; import RadioShowcase from 'ui/showcases/Radio';
import RatingShowcase from 'ui/showcases/Rating';
import SelectShowcase from 'ui/showcases/Select'; import SelectShowcase from 'ui/showcases/Select';
import SkeletonShowcase from 'ui/showcases/Skeleton'; import SkeletonShowcase from 'ui/showcases/Skeleton';
import SpinnerShowcase from 'ui/showcases/Spinner'; import SpinnerShowcase from 'ui/showcases/Spinner';
...@@ -61,6 +62,7 @@ const tabs = [ ...@@ -61,6 +62,7 @@ const tabs = [
{ label: 'Pagination', value: 'pagination', component: <PaginationShowcase/> }, { label: 'Pagination', value: 'pagination', component: <PaginationShowcase/> },
{ label: 'Progress Circle', value: 'progress-circle', component: <ProgressCircleShowcase/> }, { label: 'Progress Circle', value: 'progress-circle', component: <ProgressCircleShowcase/> },
{ label: 'Radio', value: 'radio', component: <RadioShowcase/> }, { label: 'Radio', value: 'radio', component: <RadioShowcase/> },
{ label: 'Rating', value: 'rating', component: <RatingShowcase/> },
{ label: 'Pin input', value: 'pin-input', component: <PinInputShowcase/> }, { label: 'Pin input', value: 'pin-input', component: <PinInputShowcase/> },
{ label: 'Popover', value: 'popover', component: <PopoverShowcase/> }, { label: 'Popover', value: 'popover', component: <PopoverShowcase/> },
{ label: 'Select', value: 'select', component: <SelectShowcase/> }, { label: 'Select', value: 'select', component: <SelectShowcase/> },
......
...@@ -24,7 +24,6 @@ interface Props { ...@@ -24,7 +24,6 @@ interface Props {
showUpdateMetadataItem?: boolean; showUpdateMetadataItem?: boolean;
} }
// TODO @tom2drum fix modal open on menu item click
const AccountActionsMenu = ({ isLoading, className, showUpdateMetadataItem }: Props) => { const AccountActionsMenu = ({ isLoading, className, showUpdateMetadataItem }: Props) => {
const router = useRouter(); 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