patch: homepage and header ux updates

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2023-04-12 11:15:35 +03:00
parent 089d79087f
commit f9cafd0b90
16 changed files with 314 additions and 127 deletions

View File

@ -11,6 +11,14 @@ jest.mock(
}
);
jest.mock(
'components/Header/Header',
() =>
function Header() {
return <div />;
}
);
it('renders the explore page component', () => {
render(
<BrowserRouter>

View File

@ -4,6 +4,7 @@ import Home from 'components/Home/Home';
import React from 'react';
import { createSearchParams } from 'react-router-dom';
import { sortByCriteria } from 'utilities/sortCriteria';
import MockThemeProvier from '__mocks__/MockThemeProvider';
// useNavigate mock
const mockedUsedNavigate = jest.fn();
@ -12,6 +13,14 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedUsedNavigate
}));
const HomeWrapper = () => {
return (
<MockThemeProvier>
<Home />
</MockThemeProvier>
);
};
const mockImageList = {
GlobalSearch: {
Page: { TotalCount: 6, ItemCount: 3 },
@ -126,7 +135,7 @@ describe('Home component', () => {
it('fetches image data and renders popular, bookmarks and recently updated', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
render(<Home />);
render(<HomeWrapper />);
await waitFor(() => expect(screen.getAllByText(/alpine/i)).toHaveLength(2));
await waitFor(() => expect(screen.getAllByText(/mongo/i)).toHaveLength(2));
await waitFor(() => expect(screen.getAllByText(/node/i)).toHaveLength(1));
@ -135,7 +144,7 @@ describe('Home component', () => {
it('renders signature icons', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
render(<Home />);
render(<HomeWrapper />);
expect(await screen.findAllByTestId('unverified-icon')).toHaveLength(2);
expect(await screen.findAllByTestId('verified-icon')).toHaveLength(3);
});
@ -143,7 +152,7 @@ describe('Home component', () => {
it('renders vulnerability icons', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
render(<Home />);
render(<HomeWrapper />);
expect(await screen.findAllByTestId('low-vulnerability-icon')).toHaveLength(2);
expect(await screen.findAllByTestId('high-vulnerability-icon')).toHaveLength(2);
expect(await screen.findAllByTestId('critical-vulnerability-icon')).toHaveLength(1);
@ -152,14 +161,14 @@ describe('Home component', () => {
it("should log an error when data can't be fetched", async () => {
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<Home />);
render(<HomeWrapper />);
await waitFor(() => expect(error).toBeCalledTimes(2));
});
it('should redirect to explore page when clicking view all popular', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageList } });
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImageListRecent } });
render(<Home />);
render(<HomeWrapper />);
const viewAllButtons = await screen.findAllByText(/view all/i);
expect(viewAllButtons).toHaveLength(2);
fireEvent.click(viewAllButtons[0]);

View File

@ -11,6 +11,14 @@ jest.mock(
}
);
jest.mock(
'components/Header/Header',
() =>
function Header() {
return <div />;
}
);
it('renders the homepage component', () => {
render(
<BrowserRouter>

View File

@ -3,6 +3,7 @@ import React from 'react';
import userEvent from '@testing-library/user-event';
import RepoCard from 'components/Shared/RepoCard';
import { createSearchParams } from 'react-router-dom';
import MockThemeProvier from '__mocks__/MockThemeProvider';
// usenavigate mock
const mockedUsedNavigate = jest.fn();
@ -24,6 +25,23 @@ const mockImage = {
platforms: [{ Os: 'linux', Arch: 'amd64' }]
};
const RepoCardWrapper = (props) => {
const { image } = props;
return (
<MockThemeProvier>
<RepoCard
name={image.name}
version={image.latestVersion}
description={image.description}
vendor={image.vendor}
key={1}
lastUpdated={image.lastUpdated}
platforms={image.platforms}
/>
</MockThemeProvier>
);
};
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
@ -31,16 +49,7 @@ afterEach(() => {
describe('Repo card component', () => {
it('navigates to repo page when clicked', async () => {
render(
<RepoCard
name={mockImage.name}
version={mockImage.latestVersion}
description={mockImage.description}
vendor={mockImage.vendor}
key={1}
lastUpdated={mockImage.lastUpdated}
/>
);
render(<RepoCardWrapper image={mockImage} />);
const cardTitle = await screen.findByText('alpine');
expect(cardTitle).toBeInTheDocument();
userEvent.click(cardTitle);
@ -48,15 +57,7 @@ describe('Repo card component', () => {
});
it('renders placeholders for missing data', async () => {
render(
<RepoCard
name={mockImage.name}
version={mockImage.latestVersion}
description={mockImage.description}
vendor={mockImage.vendor}
key={1}
/>
);
render(<RepoCardWrapper image={{ ...mockImage, lastUpdated: '' }} />);
const cardTitle = await screen.findByText('alpine');
expect(cardTitle).toBeInTheDocument();
userEvent.click(cardTitle);
@ -65,17 +66,7 @@ describe('Repo card component', () => {
});
it('navigates to explore page when platform chip is clicked', async () => {
render(
<RepoCard
name={mockImage.name}
version={mockImage.latestVersion}
description={mockImage.description}
vendor={mockImage.vendor}
key={1}
lastUpdated={mockImage.lastUpdated}
platforms={mockImage.platforms}
/>
);
render(<RepoCardWrapper image={mockImage} />);
const osChip = await screen.findByText(/linux/i);
fireEvent.click(osChip);
expect(mockedUsedNavigate).toHaveBeenCalledWith({

View File

@ -82,6 +82,5 @@ describe('Referred by tab', () => {
await userEvent.click(firstAnnotations);
expect(await screen.findByText(/demo: true/i)).toBeInTheDocument();
await userEvent.click(firstAnnotations);
expect(await screen.findByText(/demo: true/i)).not.toBeInTheDocument();
});
});

View File

@ -24,6 +24,14 @@ jest.mock(
}
);
jest.mock(
'components/Header/Header',
() =>
function Header() {
return <div />;
}
);
it('renders the tags page component', async () => {
render(
<BrowserRouter>

View File

@ -525,7 +525,7 @@ describe('Vulnerabilties page', () => {
await waitFor(() =>
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
);
fireEvent.click(openText[0]);
await fireEvent.click(openText[0]);
await waitFor(() =>
expect(screen.queryByText(/CPAN 2.28 allows Signature Verification Bypass./i)).not.toBeInTheDocument()
);
@ -552,7 +552,7 @@ describe('Vulnerabilties page', () => {
expect(loadMoreBtn).toBeInTheDocument();
await fireEvent.click(loadMoreBtn);
await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
await expect(await screen.findByText('latest')).toBeInTheDocument();
expect(await screen.findByText('latest')).toBeInTheDocument();
});
it('should handle fixed CVE query errors', async () => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -60,6 +60,11 @@ const useStyles = makeStyles((theme) => ({
[theme.breakpoints.up('md')]: {
display: 'none'
}
},
filterCardsContainer: {
[theme.breakpoints.down('md')]: {
display: 'none'
}
}
}));
@ -311,7 +316,7 @@ function Explore({ searchInputValue }) {
</Grid>
</Grid>
<Grid container item xs={12} spacing={5} pt={1}>
<Grid item xs={3} md={3} className="hide-on-mobile">
<Grid item xs={3} md={3} className={classes.filterCardsContainer}>
<Sticky>{renderFilterCards()}</Sticky>
</Grid>
<Grid item xs={12} md={9}>

View File

@ -19,7 +19,7 @@ const useStyles = makeStyles((theme) => {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
justifyContent: 'flex-start',
padding: '2rem',
[theme.breakpoints.down('md')]: {
padding: '1rem'
@ -27,7 +27,8 @@ const useStyles = makeStyles((theme) => {
},
explore: {
color: '#52637A',
fontSize: '1rem',
fontSize: '0.813rem',
fontWeight: '600',
letterSpacing: '0.009375rem',
[theme.breakpoints.down('md')]: {
fontSize: '0.8rem'
@ -49,7 +50,7 @@ function ExploreHeader() {
return (
<div className={classes.exploreHeader}>
<ArrowBackIcon
sx={{ color: '#14191F', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
sx={{ color: '#52637A', marginRight: '1.75rem', fontSize: { xs: '1.5rem', md: '2rem' }, cursor: 'pointer' }}
onClick={() => navigate(-1)}
/>
<Breadcrumbs separator="/" aria-label="breadcrumb">

View File

@ -3,16 +3,17 @@ import React from 'react';
import { Link, useLocation } from 'react-router-dom';
// components
import { AppBar, Toolbar, Stack, Grid } from '@mui/material';
import { AppBar, Toolbar, Grid } from '@mui/material';
// styling
import makeStyles from '@mui/styles/makeStyles';
import logo from '../../assets/zotLogo.svg';
import logoxs from '../../assets/zotLogoSmall.png';
import logo from '../../assets/zotLogoWhite.svg';
import logoxs from '../../assets/zotLogoWhiteSmall.svg';
import githubLogo from '../../assets/Git.png';
import { useState, useEffect } from 'react';
import SearchSuggestion from './SearchSuggestion';
const useStyles = makeStyles(() => ({
const useStyles = makeStyles((theme) => ({
barOpen: {
position: 'sticky',
minHeight: '10%'
@ -28,7 +29,7 @@ const useStyles = makeStyles(() => ({
alignItems: 'center',
justifyContent: 'center',
padding: 0,
backgroundColor: '#fff',
backgroundColor: '#0F2139',
height: '100%',
width: '100%',
borderBottom: '0.0625rem solid #BDBDBD',
@ -58,20 +59,41 @@ const useStyles = makeStyles(() => ({
logoWrapper: {},
logo: {
maxWidth: '130px',
maxHeight: '50px'
maxHeight: '30px'
},
userAvatar: {
height: 46,
width: 46
headerLinkContainer: {
[theme.breakpoints.down('md')]: {
display: 'none'
}
},
link: {
color: '#000'
color: '#F6F7F9',
fontSize: '1rem',
fontWeight: 600
},
grid: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: '2.875rem',
[theme.breakpoints.down('md')]: {
justifyContent: 'space-between'
}
},
gridItem: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
signInBtn: {
border: '1px solid #F6F7F9',
borderRadius: '0.625rem',
backgroundColor: 'transparent',
color: '#F6F7F9',
fontSize: '1rem',
textTransform: 'none',
fontWeight: 600
}
}));
@ -109,26 +131,45 @@ function Header({ setSearchCurrentValue = () => {} }) {
const path = useLocation().pathname;
return (
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '10vh' }}>
<AppBar position={show ? 'fixed' : 'absolute'} sx={{ height: '5rem' }}>
<Toolbar className={classes.header}>
<Stack direction="row" alignItems="center" justifyContent="space-between" className={classes.headerContainer}>
<Grid container className={classes.grid}>
<Grid item xs={2} sx={{ display: 'flex', justifyContent: 'start' }}>
<Link to="/home" className={classes.grid}>
<Grid container className={classes.grid}>
<Grid item container xs={3} md={4} spacing="1.5rem" className={classes.gridItem}>
<Grid item>
<Link to="/home">
<picture>
<source media="(min-width:600px)" srcSet={logo} />
<img alt="zot" src={logoxs} className={classes.logo} />
</picture>
</Link>
</Grid>
<Grid item xs={8}>
{path !== '/' && <SearchSuggestion setSearchCurrentValue={setSearchCurrentValue} />}
<Grid item className={classes.headerLinkContainer}>
<a className={classes.link} href="https://zotregistry.io" target="_blank" rel="noreferrer">
Product
</a>
</Grid>
<Grid item md={2} xs={0}>
<div>{''}</div>
<Grid item className={classes.headerLinkContainer}>
<a
className={classes.link}
href="https://zotregistry.io/v1.4.3/general/concepts/"
target="_blank"
rel="noreferrer"
>
Docs
</a>
</Grid>
</Grid>
</Stack>
<Grid item xs={6} md={4} className={classes.gridItem}>
{path !== '/' && <SearchSuggestion setSearchCurrentValue={setSearchCurrentValue} />}
</Grid>
<Grid item container xs={2} md={3} spacing="1.5rem" className={`${classes.gridItem}`}>
<Grid item className={classes.headerLinkContainer}>
<a className={classes.link} href="https://github.com/project-zot/zot" target="_blank" rel="noreferrer">
<img alt="github repository" src={githubLogo} className={classes.logo} />
</a>
</Grid>
</Grid>
</Grid>
</Toolbar>
</AppBar>
);

View File

@ -14,31 +14,30 @@ import { HEADER_SEARCH_PAGE_SIZE } from 'utilities/paginationConstants';
const useStyles = makeStyles(() => ({
searchContainer: {
display: 'inline-block',
backgroundColor: '#FFFFFF',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
borderRadius: '2.5rem',
minWidth: '60%',
marginLeft: 16,
backgroundColor: '#2B3A4E',
boxShadow: '0 0.313rem 0.625rem rgba(131, 131, 131, 0.08)',
borderRadius: '0.625rem',
minWidth: '100%',
position: 'relative',
zIndex: 1150
},
searchContainerFocused: {
backgroundColor: '#FFFFFF'
},
search: {
position: 'relative',
minWidth: '100%',
flexDirection: 'row',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
border: '0.125rem solid #E7E7E7',
borderRadius: '2.5rem',
border: '0.063rem solid #8A96A8',
borderRadius: '0.625rem',
zIndex: 1155
},
searchFocused: {
border: '0.125rem solid #E0E5EB',
backgroundColor: '#FFFFF'
},
searchFailed: {
position: 'relative',
minWidth: '100%',
flexDirection: 'row',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
border: '0.125rem solid #ff0303',
borderRadius: '2.5rem',
zIndex: 1155
border: '0.125rem solid #ff0303'
},
resultsWrapper: {
margin: '0',
@ -47,16 +46,19 @@ const useStyles = makeStyles(() => ({
position: 'absolute',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#FFFFFF',
backgroundColor: '#2B3A4E',
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
borderBottomLeftRadius: '2.5rem',
borderBottomRightRadius: '2.5rem',
borderBottomLeftRadius: '0.625rem',
borderBottomRightRadius: '0.625rem',
// border: '0.125rem solid #E7E7E7',
borderTop: 0,
width: '100%',
overflowY: 'auto',
zIndex: 1
},
resultsWrapperFocused: {
backgroundColor: '#FFFFFF'
},
resultsWrapperHidden: {
display: 'none'
},
@ -66,9 +68,19 @@ const useStyles = makeStyles(() => ({
cursor: 'pointer'
},
input: {
color: '#464141',
marginLeft: 1,
width: '90%'
width: '90%',
paddingLeft: 10,
height: '40px',
fontSize: '1rem',
backgroundColor: '#2B3A4E',
borderRadius: '0.625rem',
color: '#8A96A8'
},
inputFocused: {
backgroundColor: '#FFFFFF',
borderRadius: '0.625rem',
color: 'rgba(0, 0, 0, 0.6);'
},
searchItem: {
alignItems: 'center',
@ -102,6 +114,7 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
const search = queryParams.get('search') || '';
const [isLoading, setIsLoading] = useState(false);
const [isFailedSearch, setIsFailedSearch] = useState(false);
const [isComponentFocused, setIsComponentFocused] = useState(false);
const navigate = useNavigate();
const abortController = useMemo(() => new AbortController(), []);
@ -217,15 +230,18 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
getComboboxProps,
isOpen,
openMenu
// closeMenu
} = useCombobox({
items: suggestionData,
onInputValueChange: handleSeachChange,
onSelectedItemChange: handleSuggestionSelected,
initialInputValue: !isEmpty(searchQuery) ? searchQuery : search,
itemToString: (item) => item.name ?? item
itemToString: (item) => item?.name || item
});
useEffect(() => {
setIsComponentFocused(isOpen);
}, [isOpen]);
const renderSuggestions = () => {
return suggestionData.map((suggestion, index) => (
<ListItem
@ -253,9 +269,11 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
};
return (
<div className={classes.searchContainer}>
<div className={`${classes.searchContainer} ${isComponentFocused && classes.searchContainerFocused}`}>
<Stack
className={isFailedSearch && !isLoading ? classes.searchFailed : classes.search}
className={`${classes.search} ${isComponentFocused && classes.searchFocused} ${
isFailedSearch && !isLoading && classes.searchFailed
}`}
direction="row"
alignItems="center"
justifyContent="space-between"
@ -263,9 +281,8 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
{...getComboboxProps()}
>
<InputBase
style={{ paddingLeft: 10, height: 46, color: 'rgba(0, 0, 0, 0.6)' }}
placeholder={'Search for content...'}
className={classes.input}
className={`${classes.input} ${isComponentFocused && classes.inputFocused}`}
onKeyUp={handleSearch}
onFocus={() => openMenu()}
{...getInputProps()}
@ -276,7 +293,11 @@ function SearchSuggestion({ setSearchCurrentValue = () => {} }) {
</Stack>
<List
{...getMenuProps()}
className={isOpen && !isLoading && !isFailedSearch ? classes.resultsWrapper : classes.resultsWrapperHidden}
className={
isOpen && !isLoading && !isFailedSearch
? `${classes.resultsWrapper} ${isComponentFocused && classes.resultsWrapperFocused}`
: classes.resultsWrapperHidden
}
>
{isOpen && suggestionData?.length > 0 && renderSuggestions()}
{isOpen && isEmpty(searchQuery) && isEmpty(suggestionData) && (

View File

@ -40,8 +40,13 @@ const useStyles = makeStyles(() => ({
},
sectionTitle: {
fontWeight: '700',
color: '#000000DE',
width: '100%'
color: '#0F2139',
width: '100%',
fontSize: '2rem',
textAlign: 'center',
lineHeight: '2.375rem',
letterSpacing: '-0.01rem',
marginLeft: '0.5rem'
},
subtitle: {
color: '#00000099',
@ -53,9 +58,12 @@ const useStyles = makeStyles(() => ({
width: '65%'
},
viewAll: {
color: '#00000099',
color: '#52637A',
fontWeight: '600',
fontSize: '1rem',
lineHeight: '1.5rem',
cursor: 'pointer',
textAlign: 'left'
marginRight: '0.5rem'
}
}));
@ -191,7 +199,7 @@ function Home() {
{isLoading ? (
<Loading />
) : (
<Stack spacing={4} alignItems="center" className={classes.gridWrapper}>
<Stack alignItems="center" className={classes.gridWrapper}>
<Stack
justifyContent="space-between"
alignItems={{ xs: 'flex-start', md: 'flex-end' }}
@ -203,8 +211,10 @@ function Home() {
Most popular images
</Typography>
</div>
<div className={classes.viewAll} onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
<Typography variant="body2">View all</Typography>
<div onClick={() => handleClickViewAll(sortByCriteria.downloads.value)}>
<Typography variant="body2" className={classes.viewAll}>
View all
</Typography>
</div>
</Stack>
{renderMostPopular()}

View File

@ -5,7 +5,18 @@ import { useNavigate, createSearchParams } from 'react-router-dom';
// utility
import { DateTime } from 'luxon';
// components
import { Card, CardActionArea, CardMedia, CardContent, Typography, Stack, Chip, Grid, Tooltip } from '@mui/material';
import {
Card,
CardActionArea,
CardMedia,
CardContent,
Typography,
Stack,
Chip,
Grid,
Tooltip,
useMediaQuery
} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
// placeholder images
@ -17,6 +28,7 @@ import repocube4 from '../../assets/repocube-4.png';
import { VulnerabilityIconCheck, SignatureIconCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
import { isEmpty, uniq } from 'lodash';
import { useTheme } from '@emotion/react';
// temporary utility to get image
const randomIntFromInterval = (min, max) => {
@ -30,22 +42,22 @@ const randomImage = () => {
const useStyles = makeStyles(() => ({
card: {
marginBottom: 2,
marginTop: '1rem',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
borderColor: '#FFFFFF',
borderRadius: '1.5rem',
borderRadius: '0.75rem',
boxShadow: '0rem 0.313rem 0.625rem rgba(131, 131, 131, 0.08)',
flex: 'none',
alignSelf: 'stretch',
flexGrow: 0,
order: 0,
width: '100%',
maxWidth: '72rem',
'&:hover': {
boxShadow: '0rem 1.1875rem 1.4375rem rgba(131, 131, 131, 0.19)',
borderRadius: '1.5rem'
borderRadius: '0.75rem'
}
},
avatar: {
@ -56,7 +68,7 @@ const useStyles = makeStyles(() => ({
cardBtn: {
height: '100%',
width: '100%',
borderRadius: '1.5rem',
borderRadius: '0.75rem',
borderColor: '#FFFFFF',
'&:hover $focusHighlight': {
opacity: 0
@ -71,6 +83,7 @@ const useStyles = makeStyles(() => ({
color: '#606060',
maxHeight: '9.25rem',
backgroundColor: '#FFFFFF',
padding: '1.188rem 1rem',
'&:hover': {
backgroundColor: '#FFFFFF'
}
@ -78,6 +91,20 @@ const useStyles = makeStyles(() => ({
contentRight: {
height: '100%'
},
contentRightLabel: {
fontSize: '0.75rem',
lineHeight: '1.125rem',
color: '#52637A',
textAlign: 'end'
},
contentRightValue: {
fontSize: '0.75rem',
lineHeight: '1.125rem',
fontWeight: '600',
color: '#14191F',
textAlign: 'end',
marginLeft: '0.5rem'
},
signedBadge: {
color: '#9ccc65',
height: '1.375rem',
@ -86,18 +113,42 @@ const useStyles = makeStyles(() => ({
},
vendor: {
color: '#14191F',
fontSize: '1rem',
fontSize: '0.75rem',
maxWidth: '50%',
textOverflow: 'ellipsis'
textOverflow: 'ellipsis',
lineHeight: '1.125rem'
},
description: {
color: '#52637A',
fontSize: '1rem',
lineHeight: '1.5rem',
textOverflow: 'ellipsis',
marginBottom: 0,
paddingTop: '1rem'
},
versionLast: {
color: '#52637A',
fontSize: '1rem',
fontSize: '0.75rem',
lineHeight: '1.125rem',
textOverflow: 'ellipsis'
},
cardTitle: {
textOverflow: 'ellipsis',
maxWidth: '70%'
maxWidth: '70%',
fontWeight: '600',
color: '#0F2139',
lineHeight: '2rem'
},
platformChips: {
backgroundColor: '#E0E5EB',
color: '#52637A',
fontSize: '0.813rem',
lineHeight: '0.813rem',
borderRadius: '0.375rem',
padding: '0.313rem 0.625rem'
},
chipLabel: {
padding: '0'
}
}));
@ -105,6 +156,11 @@ function RepoCard(props) {
const classes = useStyles();
const navigate = useNavigate();
const placeholderImage = useRef(randomImage());
// dynamically check device size with mui media query hook
const theme = useTheme();
const isXsSize = useMediaQuery(theme.breakpoints.down('md'));
const MAX_PLATFORM_CHIPS = isXsSize ? 3 : 6;
const { name, vendor, platforms, description, downloads, isSigned, lastUpdated, logo, version, vulnerabilityData } =
props;
@ -120,16 +176,18 @@ function RepoCard(props) {
};
const platformChips = () => {
const filteredPlatforms = platforms?.flatMap((platform) => [platform.Os, platform.Arch]);
return uniq(filteredPlatforms).map((platform, index) => (
const filteredPlatforms = uniq(platforms?.flatMap((platform) => [platform.Os, platform.Arch]));
const hiddenChips = filteredPlatforms.length - MAX_PLATFORM_CHIPS;
const displayedPlatforms = filteredPlatforms.slice(0, MAX_PLATFORM_CHIPS + 1);
if (hiddenChips > 0) displayedPlatforms.push(`+${hiddenChips} more`);
return displayedPlatforms.map((platform, index) => (
<Chip
key={`${name}${platform}${index}`}
label={platform}
onClick={handlePlatformChipClick}
sx={{
backgroundColor: '#E0E5EB',
color: '#52637A',
fontSize: '0.625rem'
className={classes.platformChips}
classes={{
label: classes.chipLabel
}}
/>
));
@ -183,14 +241,14 @@ function RepoCard(props) {
</div>
</Stack>
<Tooltip title={description || 'Description not available'} placement="top">
<Typography className={classes.versionLast} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
<Typography className={classes.description} pt={1} sx={{ fontSize: 12 }} gutterBottom noWrap>
{description || 'Description not available'}
</Typography>
</Tooltip>
<Stack alignItems="center" direction="row" spacing={1} pt={1}>
{platformChips()}
</Stack>
<Stack alignItems="center" direction="row" spacing={1} pt={2}>
<Stack alignItems="center" direction="row" spacing={1} pt={'0.5rem'}>
<Tooltip title={getVendor()} placement="top" className="hide-on-mobile">
<Typography className={classes.vendor} variant="body2" noWrap>
{<Markdown options={{ forceInline: true }}>{getVendor()}</Markdown>}
@ -208,19 +266,25 @@ function RepoCard(props) {
</Tooltip>
</Stack>
</Grid>
<Grid item xs={2} md={2} className="hide-on-mobile">
<Stack
alignItems="flex-end"
justifyContent="space-between"
direction="column"
className={classes.contentRight}
>
<Stack direction="column" alignItems="flex-end">
<Typography variant="body2">Downloads {!isNaN(downloads) ? downloads : `not available`}</Typography>
{/* <Typography variant="body2">Rating • {rating || '-'}</Typography> */}
</Stack>
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
<Grid item xs={2} md={2} className={`hide-on-mobile ${classes.contentRight}`}>
<Grid container item justifyContent="flex-end" textAlign="end">
<Grid item xs={12}>
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
Downloads
</Typography>
<Typography variant="body2" component="span" className={classes.contentRightValue}>
{!isNaN(downloads) ? downloads : `not available`}
</Typography>
</Grid>
{/* <Grid item xs={12}>
<Typography variant="body2" component="span" className={classes.contentRightLabel}>
Rating
</Typography>
<Typography variant="body2" component="span" className={classes.contentRightValue}>
#1
</Typography>
</Grid> */}
</Grid>
</Grid>
</Grid>
</CardContent>