patch: homepage and header ux updates
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
parent
089d79087f
commit
f9cafd0b90
@ -11,6 +11,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the explore page component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -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]);
|
||||
|
@ -11,6 +11,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the homepage component', () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -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({
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -24,6 +24,14 @@ jest.mock(
|
||||
}
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'components/Header/Header',
|
||||
() =>
|
||||
function Header() {
|
||||
return <div />;
|
||||
}
|
||||
);
|
||||
|
||||
it('renders the tags page component', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
|
@ -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 |
14
src/assets/zotLogoWhite.svg
Normal file
14
src/assets/zotLogoWhite.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 32 KiB |
8
src/assets/zotLogoWhiteSmall.svg
Normal file
8
src/assets/zotLogoWhiteSmall.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 13 KiB |
@ -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}>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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) && (
|
||||
|
@ -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()}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user