Skip to content
This repository has been archived by the owner on Apr 5, 2024. It is now read-only.

Commit

Permalink
Merge pull request #56 from forte-music/feature/search
Browse files Browse the repository at this point in the history
Implemented Search Feature
  • Loading branch information
AzureMarker authored Aug 19, 2018
2 parents 7482a15 + 25096a0 commit 4e726fa
Show file tree
Hide file tree
Showing 24 changed files with 856 additions and 66 deletions.
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@types/enzyme": "^3.1.10",
"@types/graphql": "^0.13.0",
"@types/jest": "^23.1.0",
"@types/lodash": "^4.14.116",
"@types/react-dom": "^16.0.6",
"@types/react-redux": "^6.0.2",
"@types/react-router-dom": "^4.2.7",
Expand Down Expand Up @@ -50,6 +51,7 @@
"isomorphic-fetch": "^2.2.1",
"jest": "^23.1.0",
"jest-enzyme": "^6.0.1",
"lodash": "^4.17.10",
"object-assign": "4.1.1",
"polished": "^1.9.3",
"prettier": "1.9.2",
Expand Down Expand Up @@ -92,17 +94,14 @@
"scripts": {
"fix-style": "prettier --write $npm_package_config_prettierFiles",
"check-all": "yarn query-codegen && yarn lint && CI=true yarn test",
"storybook":
"REACT_APP_MOCK_RESOLVER=true start-storybook -p 9001 -c config/storybook",
"storybook": "REACT_APP_MOCK_RESOLVER=true start-storybook -p 9001 -c config/storybook",
"start-mock": "REACT_APP_MOCK_RESOLVER=true node scripts/start.js",
"start": "node scripts/start.js",
"build": "yarn query-codegen && node scripts/build.js",
"test": "node scripts/test.js",
"lint": "yarn tsc && yarn check-style && yarn tslint",
"tslint": "tslint --project .",
"check-style":
"prettier --list-different $npm_package_config_prettierFiles",
"query-codegen":
"apollo codegen:generate --schema node_modules/@forte-music/schema/schema.json --queries 'src/**/*.{graphql,ts,tsx}' --target typescript"
"check-style": "prettier --list-different $npm_package_config_prettierFiles",
"query-codegen": "apollo codegen:generate --schema node_modules/@forte-music/schema/schema.json --queries 'src/**/*.{graphql,ts,tsx}' --target typescript"
}
}
2 changes: 1 addition & 1 deletion src/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const Sidebar = (props: Props) => (
<SidebarLink to={songsPath}>Songs</SidebarLink>
<SidebarLink to={artistsPath}>Artists</SidebarLink>
<SidebarLink to={albumsPath}>Albums</SidebarLink>
<SidebarLink to={searchPath}>Search</SidebarLink>
<SidebarLink to={searchPath('')}>Search</SidebarLink>
</aside>
);

Expand Down
55 changes: 55 additions & 0 deletions src/components/AlbumSearchResultsContainer/enhancers/query.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';
import {
AlbumResultsQuery as Data,
AlbumResultsQueryVariables as Variables,
} from './__generated__/AlbumResultsQuery';
import gql from 'graphql-tag';
import { Omit } from '../../../utils';
import {
ConnectionQuery,
ConnectionQueryProps,
ConnectionQueryResult,
} from '../../ConnectionQuery';

export const albumSearchResultFragment = gql`
fragment AlbumSearchResults on AlbumConnection {
pageInfo {
hasNextPage
}
edges {
node {
id
artworkUrl
name
artist {
id
name
}
songs {
id
}
}
}
}
`;

const query = gql`
query AlbumResultsQuery($cursor: String, $query: String!, $first: Int!) {
albums(
first: $first
after: $cursor
sort: { filter: $query, sortBy: LEXICOGRAPHICALLY }
) @connection {
...AlbumSearchResults
}
}
${albumSearchResultFragment}
`;

export type Result = ConnectionQueryResult<Data, Variables>;

export const AlbumsQuery = (
props: Omit<ConnectionQueryProps<Data, Variables>, 'query'>
) => <ConnectionQuery query={query} children={props.children} {...props} />;
58 changes: 58 additions & 0 deletions src/components/AlbumSearchResultsContainer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import Observer from 'react-intersection-observer';
import { AlbumsQuery, Result } from './enhancers/query';
import { AlbumSearchResultsLoadingContainer } from '../AlbumSearchResultsLoadingContainer';
import { AlbumInfo } from '../AlbumsContainer/components/AlbumInfo';
import { ArtworkGrid } from '../styled/search';

interface Props {
// Whether or not to load more items when scrolled to the bottom of the page.
loadMore: boolean;

// The query which the header link navigates to and used to fetch songs.
query: string;
}

// Fetches data and renders album results of search results pages.
export const AlbumSearchResultsContainer = (props: Props) => (
<AlbumsQuery
variables={{ query: props.query, first: props.loadMore ? 30 : 6 }}
>
{(result: Result) => (
<AlbumSearchResultsLoadingContainer
query={props.query}
albums={
!result.loading
? result.data && result.data.albums.edges.map(edge => edge.node)
: undefined
}
children={albums => (
<React.Fragment>
<ArtworkGrid>
{albums.map(album => <AlbumInfo key={album.id} album={album} />)}
{props.loadMore &&
!result.loading &&
result.data &&
result.data.albums.pageInfo.hasNextPage && (
<Observer
key={'final'}
onChange={inView => {
if (!inView) {
return;
}

result.getNextPage();
}}
>
<div />
</Observer>
)}
</ArtworkGrid>
</React.Fragment>
)}
/>
)}
</AlbumsQuery>
);

// TODO: Share Loading More Logic With AlbumsPage
40 changes: 40 additions & 0 deletions src/components/AlbumSearchResultsLoadingContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import {
EmptyResult,
SearchResultTypeContainer,
SearchResultTypeHeader,
} from './styled/search';

import { Album } from './AlbumsContainer/components/AlbumInfo';
import { LinkStyled } from './LinkStyled';

interface Props {
// Query which yielded the albums below.
query: string;

// Albums which were found. Undefined when initially loading.
albums?: Album[];

children: (albums: Album[]) => React.ReactNode;
}

// Renders header for album search results. Handles the cases when `albums` is
// undefined or an empty array. Calls the `children` function when neither is
// the case.
export const AlbumSearchResultsLoadingContainer = (props: Props) => (
<SearchResultTypeContainer>
<SearchResultTypeHeader>
<LinkStyled to={`/search/${props.query}/albums`}>Albums</LinkStyled>
</SearchResultTypeHeader>

{props.albums ? (
props.albums.length ? (
props.children(props.albums)
) : (
<EmptyResult>No albums found</EmptyResult>
)
) : (
<EmptyResult>Loading...</EmptyResult>
)}
</SearchResultTypeContainer>
);
2 changes: 1 addition & 1 deletion src/components/AlbumsContainer/components/AlbumInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { ArtistLink, Artist as ArtistLinkArtist } from '../../ArtistLink';
// TODO: Click Region
// TODO: Disable Draggable

interface Album extends PlaybackAlbumArtworkAlbum, AlbumLinkAlbum {
export interface Album extends PlaybackAlbumArtworkAlbum, AlbumLinkAlbum {
artist: ArtistLinkArtist;
}

Expand Down
14 changes: 14 additions & 0 deletions src/components/AllSearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { SongSearchResultsContainer } from './SongSearchResultsContainer';
import { AlbumSearchResultsContainer } from './AlbumSearchResultsContainer';

interface Props {
query: string;
}

export const AllSearchResults = (props: Props) => (
<React.Fragment>
<AlbumSearchResultsContainer loadMore={false} query={props.query} />
<SongSearchResultsContainer loadMore={false} query={props.query} />
</React.Fragment>
);
16 changes: 13 additions & 3 deletions src/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { Albums } from '../AlbumsContainer';
import Artist from '../ArtistContainer';
import Album from '../AlbumContainer';
import { Songs } from '../SongsContainer';

import { Providers } from './Providers';
import Title from '../Title';
import { KeyboardInteraction } from '../KeyboardInteraction';
import { Search } from '../SearchContainer';

import {
albumPath,
Expand All @@ -21,10 +21,12 @@ import {
artistsPath,
homePath,
queuePath,
searchPath,
songsPath,
withIdPathParam,
withIdFromProps,
searchPath,
optionalParam,
withQueryFromProps,
} from '../../utils/paths';

const Grid = styled.div`
Expand Down Expand Up @@ -82,7 +84,15 @@ export const App = () => (

<Route exact path={homePath} />
<Route exact path={queuePath} render={() => <Queue />} />
<Route exact path={searchPath} />
<Route
path={searchPath(optionalParam('query'))}
render={withQueryFromProps(props => (
<Search
query={props.match.params.query}
setQuery={newQuery => props.history.push(searchPath(newQuery))}
/>
))}
/>

<Redirect from="/" to="/home" />
</Switch>
Expand Down
32 changes: 32 additions & 0 deletions src/components/FocusedTextInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

interface Props {
className?: string;
onKeyPress?: (event: React.KeyboardEvent<HTMLInputElement>) => void | false;
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
value?: string;
placeholder?: string;
}

// Text input focused on component mount.
export class FocusedTextInput extends React.Component<Props> {
private element: HTMLInputElement | null = null;

public componentDidMount() {
if (!this.element) {
return;
}

this.element.focus();
}

public render() {
return (
<input
{...this.props}
ref={element => (this.element = element)}
type="text"
/>
);
}
}
72 changes: 72 additions & 0 deletions src/components/InfiniteDetailSongList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { DetailRow, Song as DetailRowSong } from './DetailSongTable';
import { InfiniteSongList } from './InfiniteSongList';
import { LoadingRow } from './LoadingRow';
import { InteractiveDetailTableHeader } from './InteractiveDetailTableHeader';

import { SortBy } from './SongsContainer/enhancers/__generated__/SongsQuery';

export interface Props {
// Songs to display in the list.
songs: Song[];

// The identifier of the active song. Undefined if no song is active. The
// song with this identifier will be styled as active.
activeSongId?: string;

// If there are more elements to load. When true, loadMore is called when
// more items are needed.
hasMore: boolean;

// Called to load more items.
loadMore: () => void;

// True when more items are being loaded. A loading row is displayed as
// the last row of the list when true.
isLoadingMore: boolean;

// Field which is currently being sorted by.
sortBy: SortBy;

// Update the currently sorted by field.
setSortBy: (newSort: SortBy) => void;

// Whether or not the sort is reversed.
isReverse: boolean;

// Update the sort direction.
setReverse: (newReverse: boolean) => void;

// Starts playing all the songs in the view sorted order starting from the
// song at index. Called when a song is double clicked.
startPlayingFrom: (index: number) => void;
}

interface Song extends DetailRowSong {
id: string;
}

export const InfiniteDetailSongList = (props: Props) => (
<InfiniteSongList
rows={props.songs}
hasMoreRows={props.hasMore}
loadMoreRows={props.loadMore}
isLoadingMore={props.isLoadingMore}
render={(song, index) => (
<DetailRow
song={song}
active={song.id === props.activeSongId}
onDoubleClick={() => props.startPlayingFrom(index)}
/>
)}
loading={<LoadingRow />}
header={
<InteractiveDetailTableHeader
sortBy={props.sortBy}
setSortBy={props.setSortBy}
isReverse={props.isReverse}
setReverse={props.setReverse}
/>
}
/>
);
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import { TableHeader } from '../../BaseSongTable';
import Chevron from '../../icons/Chevron';
import { TableHeader } from './BaseSongTable';
import Chevron from './icons/Chevron';
import {
AlbumColumn,
ArtistsColumn,
DurationColumn,
SongColumn,
} from '../../DetailSongTable';
} from './DetailSongTable';

import { SortBy } from '../enhancers/__generated__/SongsQuery';
import { SortBy } from './SongsContainer/enhancers/__generated__/SongsQuery';

interface HeaderProps {
isReverse: boolean;
Expand Down
Loading

0 comments on commit 4e726fa

Please sign in to comment.