Commit e2081d49 authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Merge pull request #2198 from blockscout/rating-count

Add rating count
parents 54a512b8 6a330951
<svg viewBox="0 0 15 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.502 11.242a2.491 2.491 0 0 0-1.802.77L4.919 8.758c.112-.42.112-.863 0-1.284L10.7 4.222a2.492 2.492 0 1 0-.614-1.088L4.303 6.386a2.5 2.5 0 1 0 0 3.461l5.781 3.253a2.5 2.5 0 1 0 2.417-1.858Z" fill="currentColor"/>
</svg>
......@@ -6,6 +6,7 @@ export const ratings = {
fields: {
appId: apps[0].id,
rating: 4.3,
count: 15,
},
},
],
......
......@@ -117,6 +117,7 @@
| "score/score-not-ok"
| "score/score-ok"
| "search"
| "share"
| "social/canny"
| "social/coingecko"
| "social/coinmarketcap"
......
......@@ -29,6 +29,7 @@ export type MarketplaceAppOverview = MarketplaceAppPreview & MarketplaceAppSocia
export type AppRating = {
recordId: string;
value: number | undefined;
count?: number;
}
export type MarketplaceAppWithSecurityReport = MarketplaceAppOverview & {
......
......@@ -9,13 +9,13 @@ type Props = {
}
const FavoriteIcon = ({ isFavorite, color }: Props) => {
const heartFilledColor = useColorModeValue('blue.700', 'gray.400');
const defaultColor = isFavorite ? heartFilledColor : 'gray.400';
const heartFilledColor = useColorModeValue('blue.600', 'blue.300');
const defaultColor = isFavorite ? heartFilledColor : (color || 'gray.400');
return (
<IconSvg
name={ isFavorite ? 'heart_filled' : 'heart_outline' }
color={ color || defaultColor }
color={ defaultColor }
boxSize={ 5 }
/>
);
......
......@@ -5,6 +5,8 @@ import React, { useCallback } from 'react';
import type { MarketplaceAppWithSecurityReport, ContractListTypes, AppRating } from 'types/client/marketplace';
import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AppSecurityReport from './AppSecurityReport';
import FavoriteIcon from './FavoriteIcon';
......@@ -168,7 +170,7 @@ const MarketplaceAppCard = ({
>
More info
</Link>
<Flex alignItems="center" gap={ 3 }>
<Flex alignItems="center">
<Rating
appId={ id }
rating={ rating }
......@@ -188,6 +190,21 @@ const MarketplaceAppCard = ({
h={{ base: 6, md: '30px' }}
onClick={ handleFavoriteClick }
icon={ <FavoriteIcon isFavorite={ isFavorite }/> }
ml={ 2 }
/>
<CopyToClipboard
text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' }
icon="share"
size={ 4 }
variant="ghost"
colorScheme="gray"
w={{ base: 6, md: '30px' }}
h={{ base: 6, md: '30px' }}
color="gray.400"
_hover={{ color: 'gray.400' }}
ml={{ base: 1, md: 0 }}
display="inline-flex"
borderRadius="base"
/>
</Flex>
</Flex>
......
......@@ -10,7 +10,9 @@ import { ContractListTypes } from 'types/client/marketplace';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import isBrowser from 'lib/isBrowser';
import * as mixpanel from 'lib/mixpanel/index';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
......@@ -113,6 +115,8 @@ const MarketplaceAppModal = ({
} catch (err) {}
}
const iconColor = useColorModeValue('blue.600', 'gray.400');
return (
<Modal
isOpen={ Boolean(data.id) }
......@@ -206,8 +210,23 @@ const MarketplaceAppModal = ({
colorScheme="gray"
w={ 9 }
h={ 8 }
flexShrink={ 0 }
onClick={ handleFavoriteClick }
icon={ <FavoriteIcon isFavorite={ isFavorite } color={ useColorModeValue('blue.700', 'gray.400') }/> }
icon={ <FavoriteIcon isFavorite={ isFavorite } color={ iconColor }/> }
/>
<CopyToClipboard
text={ isBrowser() ? window.location.origin + `/apps/${ id }` : '' }
icon="share"
size={ 4 }
variant="outline"
colorScheme="gray"
w={ 9 }
h={ 8 }
color={ iconColor }
_hover={{ color: iconColor }}
display="inline-flex"
borderRadius="base"
/>
</Flex>
</Flex>
......
......@@ -53,7 +53,7 @@ const MarketplaceAppTopBar = ({ appId, data, isLoading, securityReport }: Props)
return (
<>
<Flex alignItems="center" flexWrap="wrap" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
<Flex alignItems="center" mb={{ base: 3, md: 2 }} rowGap={ 3 } columnGap={ 2 }>
{ !isMobile && <NetworkLogo isCollapsed/> }
<Tooltip label="Back to dApps list">
<LinkInternal display="inline-flex" href={ goBackUrl } h="32px" isLoading={ isLoading } ml={ isMobile ? 0 : 4 }>
......
......@@ -45,10 +45,7 @@ const MarketplaceList = ({
return apps.length > 0 ? (
<>
<Grid
templateColumns={{
md: 'repeat(auto-fill, minmax(230px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
templateColumns={{ md: 'repeat(auto-fill, minmax(270px, 1fr))' }}
autoRows="1fr"
gap={{ base: '16px', md: '24px' }}
marginTop={{ base: 0, lg: 3 }}
......
......@@ -51,6 +51,7 @@ const Rating = ({
<>
<Stars filledIndex={ (rating?.value || 0) - 1 }/>
<Text fontSize="md" ml={ 2 }>{ rating?.value }</Text>
{ rating?.count && <Text variant="secondary" fontSize="md" ml={ 1 }>({ rating?.count })</Text> }
</>
) }
<Box ref={ popoverRef }>
......@@ -58,6 +59,7 @@ const Rating = ({
<PopoverTrigger>
<TriggerButton
rating={ rating?.value }
count={ rating?.count }
fullView={ fullView }
isActive={ isOpen }
onClick={ onToggle }
......
import { Button, chakra, useColorModeValue, Tooltip, useDisclosure } from '@chakra-ui/react';
import { Button, chakra, useColorModeValue, Tooltip, useDisclosure, Text } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -7,6 +7,7 @@ import IconSvg from 'ui/shared/IconSvg';
type Props = {
rating?: number;
count?: number;
fullView?: boolean;
isActive: boolean;
onClick: () => void;
......@@ -24,7 +25,7 @@ const getTooltipText = (canRate: boolean | undefined) => {
};
const TriggerButton = (
{ rating, fullView, isActive, onClick, canRate }: Props,
{ rating, count, fullView, isActive, onClick, canRate }: Props,
ref: React.ForwardedRef<HTMLButtonElement>,
) => {
const textColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
......@@ -75,8 +76,9 @@ const TriggerButton = (
/>
) }
{ (rating && !fullView) ? (
<chakra.span color={ textColor } transition="inherit">
<chakra.span color={ textColor } transition="inherit" display="inline-flex">
{ rating }
<Text variant="secondary" ml={ 1 }>({ count })</Text>
</chakra.span>
) : (
'Rate it!'
......
......@@ -28,11 +28,12 @@ export type RateFunction = (
function formatRatings(data: Airtable.Records<Airtable.FieldSet>) {
return data.reduce((acc: Record<string, AppRating>, record) => {
const fields = record.fields as { appId: string | Array<string>; rating: number | undefined };
const fields = record.fields as { appId: string | Array<string>; rating: number | undefined; count?: number };
const appId = Array.isArray(fields.appId) ? fields.appId[0] : fields.appId;
acc[appId] = {
recordId: record.id,
value: fields.rating,
count: fields.count,
};
return acc;
}, {});
......@@ -63,7 +64,7 @@ export default function useRatings() {
return;
}
try {
const data = await airtable('apps_ratings').select({ fields: [ 'appId', 'rating' ] }).all();
const data = await airtable('apps_ratings').select({ fields: [ 'appId', 'rating', 'count' ] }).all();
const ratings = formatRatings(data);
setRatings(ratings);
} catch (error) {
......
......@@ -22,7 +22,7 @@ test.beforeEach(async({ mockConfigResponse, mockEnvs, mockAssetResponse, page })
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', MARKETPLACE_CONFIG_URL, JSON.stringify(appsMock));
await mockConfigResponse('NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', MARKETPLACE_SECURITY_REPORTS_URL, JSON.stringify(securityReportsMock));
await Promise.all(appsMock.map(app => mockAssetResponse(app.logo, './playwright/mocks/image_s.jpg')));
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating', (route) => route.fulfill({
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating&fields%5B%5D=count', (route) => route.fulfill({
status: 200,
body: JSON.stringify(ratingsMock),
}));
......
......@@ -35,7 +35,7 @@ const testFn: Parameters<typeof test>[1] = async({ render, mockConfigResponse, m
Method: 'eth_chainId',
ReturnType: numberToHex(Number(config.chain.id)),
});
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating', (route) => route.fulfill({
await page.route('https://api.airtable.com/v0/test/apps_ratings?fields%5B%5D=appId&fields%5B%5D=rating&fields%5B%5D=count', (route) => route.fulfill({
status: 200,
body: JSON.stringify(ratingsMock),
}));
......
import { IconButton, Tooltip, useClipboard, chakra, useDisclosure, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
export interface Props {
......@@ -10,14 +11,18 @@ export interface Props {
onClick?: (event: React.MouseEvent) => void;
size?: number;
type?: 'link';
icon?: IconName;
variant?: string;
colorScheme?: string;
}
const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type }: Props) => {
const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type, icon, variant = 'simple', colorScheme }: Props) => {
const { hasCopied, onCopy } = useClipboard(text, 1000);
const [ copied, setCopied ] = useState(false);
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('gray.400', 'gray.500');
const iconName = icon || (type === 'link' ? 'link' : 'copy');
useEffect(() => {
if (hasCopied) {
......@@ -40,10 +45,11 @@ const CopyToClipboard = ({ text, className, isLoading, onClick, size = 5, type }
<Tooltip label={ copied ? 'Copied' : `Copy${ type === 'link' ? ' link ' : ' ' }to clipboard` } isOpen={ isOpen || copied }>
<IconButton
aria-label="copy"
icon={ <IconSvg name={ type === 'link' ? 'link' : 'copy' } boxSize={ size }/> }
icon={ <IconSvg name={ iconName } boxSize={ size }/> }
boxSize={ size }
color={ iconColor }
variant="simple"
variant={ variant }
colorScheme={ colorScheme }
display="inline-block"
flexShrink={ 0 }
onClick={ handleClick }
......
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