Skip to content

Commit

Permalink
feat(search): add search screen
Browse files Browse the repository at this point in the history
  • Loading branch information
ChristiaanScheermeijer committed Jun 10, 2021
1 parent b30ce66 commit f95fdd4
Show file tree
Hide file tree
Showing 19 changed files with 403 additions and 87 deletions.
12 changes: 6 additions & 6 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,23 +16,23 @@ class App extends Component {
error: null,
};

componentDidCatch(error: Error) {
componentDidCatch (error: Error) {
this.setState({ error });
}

render() {
render () {
return (
<QueryProvider>
<ConfigProvider
configLocation={window.configLocation}
onLoading={(isLoading: boolean) => console.info(`Loading config: ${isLoading}`)}
onValidationError={(error: Error) => console.error(`Config ${error}`)}
>
<UIStateProvider>
<Router>
<Router>
<UIStateProvider>
<Root error={this.state.error} />
</Router>
</UIStateProvider>
</UIStateProvider>
</Router>
</ConfigProvider>
</QueryProvider>
);
Expand Down
5 changes: 2 additions & 3 deletions src/components/CardGrid/CardGrid.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.cardGrid {
display: grid;
grid-gap: 24px 16px;
.cell {
padding: 8px;
}
13 changes: 9 additions & 4 deletions src/components/CardGrid/CardGrid.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import * as React from 'react';
import { render } from '@testing-library/react';

import { generatePlaylistPlaceholder } from '../../utils/collection';

import CardGrid from './CardGrid';

describe('<CardGrid>', () => {
it('renders card grid and children', () => {
const { getByText } = render(<CardGrid>aa</CardGrid>);
const cardGrid = getByText(/aa/i);
expect(document.body.contains(cardGrid));
it('renders and matches snapshot', () => {
const placeholderData = generatePlaylistPlaceholder();
const { container } = render((
<CardGrid playlist={placeholderData.playlist} onCardHover={jest.fn()} onCardClick={jest.fn()} isLoading={false} />
));

expect(container).toMatchSnapshot();
});
});
60 changes: 43 additions & 17 deletions src/components/CardGrid/CardGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,60 @@
import React from 'react';
import type { GridCellProps } from 'react-virtualized';

import useBreakpoint, { Breakpoint } from '../../hooks/useBreakpoint';
import type { PlaylistItem } from '../../../types/playlist';
import VirtualizedGrid from '../VirtualizedGrid/VirtualizedGrid';
import Card from '../Card/Card';
import useBreakpoint, { Breakpoint, Breakpoints } from '../../hooks/useBreakpoint';
import { chunk, findPlaylistImageForWidth } from '../../utils/collection';

import styles from './CardGrid.module.scss';

// TEMP DATA
const cols = {
const defaultCols: Breakpoints = {
[Breakpoint.xs]: 2,
[Breakpoint.sm]: 3,
[Breakpoint.md]: 4,
[Breakpoint.lg]: 5,
[Breakpoint.xl]: 6,
[Breakpoint.sm]: 2,
[Breakpoint.md]: 3,
[Breakpoint.lg]: 4,
[Breakpoint.xl]: 5,
};

type CardGridProps = {
children: React.ReactNode;
playlist: PlaylistItem[];
onCardHover: (item: PlaylistItem) => void;
onCardClick: (item: PlaylistItem) => void;
isLoading: boolean;
cols?: Breakpoints;
};

function CardGrid({ children }: CardGridProps) {
function CardGrid ({ playlist, onCardClick, onCardHover, isLoading = false, cols = defaultCols }: CardGridProps) {
const breakpoint: Breakpoint = useBreakpoint();
const isLargeScreen = breakpoint >= Breakpoint.md;
const imageSourceWidth = 320 * (window.devicePixelRatio > 1 || isLargeScreen ? 2 : 1);
const rows = chunk<PlaylistItem>(playlist, cols[breakpoint]);

const cellRenderer = ({ columnIndex, rowIndex, style }: GridCellProps) => {
if (!rows[rowIndex][columnIndex]) return;

const playlistItem: PlaylistItem = rows[rowIndex][columnIndex];
const { mediaid, title, duration, seriesId } = playlistItem;

return (
<div className={styles.cell} style={style} key={mediaid}>
<Card
key={mediaid}
title={title}
duration={duration}
posterSource={findPlaylistImageForWidth(playlistItem, imageSourceWidth)}
seriesId={seriesId}
onClick={() => onCardClick(playlistItem)}
onHover={() => onCardHover(playlistItem)}
loading={isLoading}
/>
</div>
);
};

return (
<div
className={styles.cardGrid}
style={{
gridTemplateColumns: `repeat(${cols[breakpoint]}, minmax(0,1fr))`,
}}
>
{children}
</div>
<VirtualizedGrid rowCount={rows.length} cols={cols} cellRenderer={cellRenderer} spacing={50} />
);
}

Expand Down
32 changes: 32 additions & 0 deletions src/components/CardGrid/__snapshots__/CardGrid.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<CardGrid> renders and matches snapshot 1`] = `
<div>
<div
style="overflow: visible; width: 0px;"
>
<div
aria-label="grid"
aria-readonly="true"
class="ReactVirtualized__Grid grid"
role="grid"
style="box-sizing: border-box; direction: ltr; height: auto; position: relative; width: 0px; will-change: transform; overflow-x: hidden; overflow-y: hidden;"
tabindex="-1"
/>
</div>
<div
class="resize-triggers"
>
<div
class="expand-trigger"
>
<div
style="width: 1px; height: 1px;"
/>
</div>
<div
class="contract-trigger"
/>
</div>
</div>
`;
39 changes: 39 additions & 0 deletions src/components/ErrorPage/ErrorPage.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
@use '../../styles/variables';
@use '../../styles/theme';
@use '../../styles/mixins/responsive';

.errorPage {
display: flex;
align-items: center;
justify-content: center;
height: 70vh;
}

.box {
max-width: 500px;
padding: 12px;
font-family: theme.$body-font-family;
}

.title {
margin-bottom: 24px;
font-size: 34px;
font-weight: 700;
color: variables.$white;
}

.main {
color: variables.$white;

> h6 {
margin-bottom: 16px;
font-weight: 700;
font-size: 20px;
}
}

@include responsive.mobile-only() {
.title {
font-size: 24px;
}
}
12 changes: 12 additions & 0 deletions src/components/ErrorPage/ErrorPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { render } from '@testing-library/react';

import ErrorPage from './ErrorPage';

describe('<ErrorPage>', () => {
test('renders and matches snapshot', () => {
const { container } = render(<ErrorPage title="This is the title">This is the content</ErrorPage>);

expect(container).toMatchSnapshot();
});
});
25 changes: 25 additions & 0 deletions src/components/ErrorPage/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

import styles from './ErrorPage.module.scss';

type Props = {
title: string;
children?: React.ReactNode;
};

const ErrorPage: React.FC<Props> = ({ title, children }: Props) => {
return (
<div className={styles.errorPage}>
<div className={styles.box}>
<header>
<h1 className={styles.title}>{title}</h1>
</header>
<main className={styles.main}>
{children}
</main>
</div>
</div>
);
};

export default ErrorPage;
26 changes: 26 additions & 0 deletions src/components/ErrorPage/__snapshots__/ErrorPage.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<ErrorPage> renders and matches snapshot 1`] = `
<div>
<div
class="errorPage"
>
<div
class="box"
>
<header>
<h1
class="title"
>
This is the title
</h1>
</header>
<main
class="main"
>
This is the content
</main>
</div>
</div>
</div>
`;
14 changes: 13 additions & 1 deletion src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Props = {
logoSrc?: string;
searchBarProps: SearchBarProps;
searchEnabled: boolean;
onCloseSearchButtonClick?: () => void;
};

const Header: React.FC<Props> = (
Expand All @@ -31,6 +32,7 @@ const Header: React.FC<Props> = (
logoSrc,
searchBarProps,
searchEnabled,
onCloseSearchButtonClick,
}
) => {
const [mobileSearchActive, setMobileSearchActive] = useState(false);
Expand All @@ -43,7 +45,17 @@ const Header: React.FC<Props> = (
mobileSearchActive ? (
<div className={styles.mobileSearch}>
<SearchBar {...searchBarProps} />
<IconButton className={styles.iconButton} aria-label="Close search" onClick={() => setMobileSearchActive(false)}>
<IconButton
className={styles.iconButton}
aria-label="Close search"
onClick={() => {
setMobileSearchActive(false);

if (onCloseSearchButtonClick) {
onCloseSearchButtonClick();
}
}}
>
<CloseIcon />
</IconButton>
</div>
Expand Down
8 changes: 4 additions & 4 deletions src/components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ type LayoutProps = {

const Layout: FC<LayoutProps> = ({ children }) => {
const { menu, assets, options, siteName, description, footerText, searchPlaylist } = useContext(ConfigContext);
const { blurImage } = useContext(UIStateContext);
const { blurImage, searchQuery, updateSearchQuery, resetSearchQuery } = useContext(UIStateContext);
const [sideBarOpen, setSideBarOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const hasDynamicBlur = options.dynamicBlur === true;
const banner = assets.banner;

Expand Down Expand Up @@ -49,9 +48,10 @@ const Layout: FC<LayoutProps> = ({ children }) => {
searchEnabled={!!searchPlaylist}
searchBarProps={{
query: searchQuery,
onQueryChange: (event) => setSearchQuery(event.target.value),
onClearButtonClick: () => setSearchQuery(''),
onQueryChange: (event) => updateSearchQuery(event.target.value),
onClearButtonClick: () => updateSearchQuery(''),
}}
onCloseSearchButtonClick={() => resetSearchQuery()}
/>
<Sidebar
isOpen={sideBarOpen}
Expand Down
2 changes: 2 additions & 0 deletions src/components/Root/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Home from '../../screens/Home/Home';
import Playlist from '../../screens/Playlist/Playlist';
import Settings from '../../screens/Settings/Settings';
import Movie from '../../screens/Movie/Movie';
import Search from '../../screens/Search/Search';

type Props = {
error?: Error | null;
Expand All @@ -25,6 +26,7 @@ const Root: FC<Props> = ({ error }: Props) => {
<Route path="/u" component={Settings} exact />
<Route path="/m/:id/:slug" component={Movie} exact />
<Route path="/s/:id/:slug" component={Series} />
<Route path="/q/:query?" component={Search} />
</Switch>
</Layout>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Shelf/Shelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const Shelf: React.FC<ShelfProps> = ({
doSlide();
};

if (error) return <h2 className={styles.error}>Could not load items</h2>;
if (error || !playlist?.playlist) return <h2 className={styles.error}>Could not load items</h2>;

return (
<div className={classNames(styles.shelf, { [styles.featured]: featured })}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/TileDock/TileDock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const TileDock = <T extends unknown>({
} as Position);
const frameRef = useRef<HTMLUListElement>() as React.MutableRefObject<HTMLUListElement>;
const tileWidth: number = 100 / tilesToShow;
const isMultiPage: boolean = items.length > tilesToShow;
const isMultiPage: boolean = items?.length > tilesToShow;
const transformWithOffset: number = isMultiPage ? 100 - tileWidth * (tilesToShow + 1) + transform : 0;

const tileList: Tile<T>[] = useMemo(() => {
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/useFirstRender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useEffect, useRef } from 'react';

const useFirstRender = () => {
const firstRenderRef = useRef(true);

useEffect(() => {
firstRenderRef.current = false;
}, []);

return firstRenderRef.current;
};

export default useFirstRender;
Loading

0 comments on commit f95fdd4

Please sign in to comment.