Skip to content

Commit

Permalink
Merge pull request #129 from Giphy/feat/search-bar-release
Browse files Browse the repository at this point in the history
Feat/search bar release
  • Loading branch information
giannif authored Sep 3, 2020
2 parents 4a181e2 + 1beec48 commit f9cf7ca
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 41 deletions.
89 changes: 89 additions & 0 deletions packages/react-components/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ ReactDOM.render(<Carousel gifHeight={200} gutter={6} fetchGifs={fetchGifs} />, t

## Gif

Displays a single GIF. The building block of the [Grid](#grid) and [Carousel](#carousel). If you want to build a custom layout component,
using this will make it easy to do so.

_Gif props_

| _prop_ | _type_ | _default_ | _description_ |
Expand All @@ -116,6 +119,92 @@ const { data } = await gf.gif('fpXxIjftmkk9y')
ReactDOM.render(<Gif gif={data} width={300} />, target)
```

### Search Experience

The search experience is built on a search bar and a separate visual component that can display content, which can be a [Grid](#grid) or [Carousel](#carousel), or a custom component that can fetch and render an array of `IGif` objects. To create the search experience, we use `React.Context` to set up communication between the search bar and other components by defining them as children of a [SearchContextManager](#searchcontextmanager). We recommend using the [SuggestionBar](#suggestionbar) to display trending searches and enable username searches when a user searches by username (e.g. `@nba`).

See [codesandbox](https://codesandbox.io/s/giphyreact-components-hbmcf?from-embed) for runnable code

```tsx
import {
Grid, // our UI Component to display the results
SearchBar, // the search bar the user will type into
SearchContext, // the context that wraps and connects our components
SearchContextManager, // the context manager, includes the Context.Provider
SuggestionBar, // an optional UI component that displays trending searches and channel / username results
} from '@giphy/react-components'

// the search experience consists of the manager and its child components that use SearchContext
const SearchExperience = () => (
<SearchContextManager apiKey={apiKey}>
<Components />
</SearchContextManager>
)

// define the components in a separate function so we can
// use the context hook. You could also use the render props pattern
const Components = () => {
const { fetchGifs, searchKey } = useContext(SearchContext)
return (
<>
<SearchBarComponent />
<SuggestionBar />
<Grid key={searchKey} columns={3} width={800} fetchGifs={fetchGifs} />
</>
)
}
```

#### SearchContextManager

This component manages the [SearchContext](#searchcontext) that the child components access.

It has a few initialization props:

| _prop_ | _type_ | _default_ | _description_ |
| ----------- | ---------------------------------------------------------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------- |
| apiKey | string | undefined | Your api key |
| initialTerm | string | '' | _Advanced usage_ a search term to fetch and render when the component is mounted |
| theme | [SearchTheme](#searchtheme) | default theme | A few theming options such as search bar height and dark or light mode |
| options | [SearchOptions](https://github.com/Giphy/giphy-js/blob/master/packages/fetch-api/README.md#search-options) | undefined | Search options that will be passed on to the search request |

#### Searchbar

An input field used in the [Search Experience](#search-experience).

| _prop_ | _type_ | _default_ | _description_ |
| ----------- | ------------- | -------------- | --------------------------------------------------------------------------------- |
| placeholder | `string` | `Search GIPHY` | The text displayed when no text is entered |
| theme | `SearchTheme` | default theme | See (SearchTheme)[#searchtheme] |
| clear | `boolean` | false | _Advanced useage_ - clears the input but will leave the term in the SearchContext |

#### SearchContext

The `SearchContext` manages the state of the search experience. The props below are all you need to configure your UI component. See (Search Experience)[#search-experience].
It should use `searchKey` as its key, so when we have a new search, the old content is removed. And it will need a `fetchGifs` to initiate the first fetch and for subsequent infinite scroll fetches

| _prop_ | _type_ | _default_ | _description_ |
| --------- | ----------------------------------------- | -------------------- | ----------------------------------------------------------------------------------- |
| searchKey | string | undefined | A unique id of the current search, used as a React key to refresh the [Grid](#grid) |
| fetchGifs | `(offset: number) => Promise<GifsResult>` | default search fetch | The search request passed to the UI component |

#### SearchTheme

Theme is passed to the [SearchContextManager](#searchcontextmanager)

| _prop_ | _type_ | _default_ | _description_ |
| -------------------- | ----------------- | --------- | --------------------------------------------------------- |
| mode | `dark` \| `light` | `light` | dark or light |
| searchbarHeight | number | 42 | Height of the search bar |
| smallSearchbarHeight | number | 35 | Height of the search bar when matching mobile media query |

#### SuggestionBar

Display scrolling trending searches and username search. When clicking a trending search term, the search input will be
populated with that term and the search will be fetched and rendered.

If a user types a username into the search bar such as `@nba`, a username search will done and the all the channels that match will be displayed in the suggestion bar. When clicking a username, the search bar will go into username search mode.

### GifOverlay

The overlay prop, available on all components allows you to overlay a gif with a custom UI and respond to hover events (desktop only).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ const SearchContextManager = ({ children, options = {}, apiKey, theme, initialTe
}
const fetchChannelSearch = async (offset: number) => {
const result = await fetch(
`https://api.giphy.com/v1/channels/search?q=${channelSearch}&offset=${offset}&api_key=${apiKey}`
`https://api.giphy.com/v1/channels/search?q=${encodeURIComponent(
channelSearch
)}&offset=${offset}&api_key=${apiKey}`
)
const { data } = await result.json()
return data as IChannel[]
Expand Down
11 changes: 6 additions & 5 deletions packages/react-components/src/components/search-bar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { css } from '@emotion/core'
import styled from '@emotion/styled'
import { giphyIndigo, giphyLightGrey } from '@giphy/js-brand'
import { giphyBlack, giphyCharcoal, giphyIndigo, giphyLightGrey, giphyWhite } from '@giphy/js-brand'
import React, { useContext, useEffect, useRef, useState } from 'react'
import useDebounce from 'react-use/lib/useDebounce'
import { SearchContext } from './context'
import SearchBarChannel from './search-bar-channel'
import SearchButton from './search-button'
import { getSize, SearchTheme } from './theme'
import styled, { getSize } from './theme'

function usePrevious<T>(value: T) {
const ref = useRef<T>(value)
Expand Down Expand Up @@ -106,10 +105,11 @@ const SearchBar = ({ className, placeholder = 'Search GIPHY', clear = false }: P
const Container = styled.div`
display: flex;
background: white;
${(props) => getSize(props.theme as SearchTheme)}
${(props) => getSize(props.theme)}
`

const Input = styled.input<{ isUsernameSearch: boolean }>`
background: ${(props) => (props.theme.mode === 'dark' ? giphyCharcoal : giphyWhite)};
box-sizing: border-box;
border: 0;
appearance: none;
Expand All @@ -119,8 +119,9 @@ const Input = styled.input<{ isUsernameSearch: boolean }>`
padding: 0 10px;
border-radius: 0;
text-overflow: ellipsis;
color: ${(props) => (props.theme.mode === 'dark' ? giphyWhite : giphyBlack)};
&::placeholder {
color: ${giphyLightGrey};
color: ${(props) => (props.theme.mode === 'dark' ? giphyLightGrey : giphyLightGrey)};
}
min-width: 150px;
flex: 1;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,35 @@
import { keyframes } from '@emotion/core'
import styled from '@emotion/styled'
import { giphyDarkCharcoal, giphyLightestGrey } from '@giphy/js-brand'
import { giphyCharcoal, giphyDarkCharcoal, giphyLightestGrey, giphyWhite } from '@giphy/js-brand'
import React, { useContext } from 'react'
import Avatar_ from '../attribution/avatar'
import VerifiedBadge from '../attribution/verified-badge'
import { SearchContext } from './context'
import { mobileQuery, SearchTheme } from './theme'
import styled, { mobileQuery, SearchTheme } from './theme'

const channelMargin = 6

const channelSearchHeight = (theme: SearchTheme) => theme.searchbarHeight - channelMargin * 2
const smallChannelSearchHeight = (theme: SearchTheme) => theme.smallSearchbarHeight - 3 * 2

const animateAvatar = (h: number) => keyframes`
to {
width: ${h}px;
}
`

const Avatar = styled(Avatar_)`
height: ${(props) => (props.theme as SearchTheme).channelSearch}px;
height: ${(props) => channelSearchHeight(props.theme)}px;
margin: 0;
width: 0;
animation: ${(props) => animateAvatar((props.theme as SearchTheme).channelSearch)} 100ms ease-in-out forwards;
animation: ${(props) => animateAvatar(channelSearchHeight(props.theme))} 100ms ease-in-out forwards;
@media (${mobileQuery}) {
height: ${(props) => (props.theme as SearchTheme).smallChannelSearch}px;
animation: ${(props) => animateAvatar((props.theme as SearchTheme).smallChannelSearch)} 100ms ease-in-out
forwards;
height: ${(props) => smallChannelSearchHeight(props.theme)}px;
animation: ${(props) => animateAvatar(smallChannelSearchHeight(props.theme))} 100ms ease-in-out forwards;
}
`

const Username = styled.div`
background: white;
background: ${(props) => (props.theme.mode === 'dark' ? giphyCharcoal : giphyWhite)};
display: flex;
align-items: center;
padding-left: ${channelMargin}px;
Expand All @@ -43,7 +44,7 @@ const UsernamePill = styled.div`
font-weight: 600;
font-size: 12px;
align-items: center;
height: ${(props) => (props.theme as SearchTheme).channelSearch}px;
height: ${(props) => channelSearchHeight(props.theme)}px;
@media (${mobileQuery}) {
display: none;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { keyframes } from '@emotion/core'
import styled from '@emotion/styled'
import React, { useContext } from 'react'
import useThrottle from 'react-use/lib/useThrottle'
import { SearchContext } from './context'
import SearchIcon_ from './search-icon'
import { getSize, SearchTheme } from './theme'
import styled, { getSize } from './theme'

const time = '2s'
const purp = '#9933FF'
Expand Down Expand Up @@ -56,7 +55,7 @@ const Container = styled.div`
@media screen and (-ms-high-contrast: active), screen and (-ms-high-contrast: none) {
display: none;
}
${(props) => getSize(props.theme as SearchTheme, true)}
${(props) => getSize(props.theme, true)}
`

const GradientBox = styled.div`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import styled from '@emotion/styled'
import { IChannel } from '@giphy/js-types'
import React, { useContext, useEffect, useState } from 'react'
import { SearchContext } from '../context'
import { getSize, SearchTheme } from '../theme'
import styled, { getSize } from '../theme'
import { ChannelPill, TrendingSearchPill } from './pills'

const Container = styled.div`
Expand All @@ -16,7 +15,7 @@ const Container = styled.div`
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 10px;
${(props) => getSize(props.theme as SearchTheme)}
${(props) => getSize(props.theme)}
`

const SuggestionBar = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import styled from '@emotion/styled'
import { giphyDarkestGrey } from '@giphy/js-brand'
import { IChannel } from '@giphy/js-types'
import React, { useContext } from 'react'
import Avatar_ from '../../attribution/avatar'
import VerifiedBadge from '../../attribution/verified-badge'
import { SearchContext } from '../context'
import { getSize, SearchTheme } from '../theme'
import styled, { getSize } from '../theme'
import TrendingIcon_ from './trending-icon'

const margin = 9
Expand All @@ -31,7 +30,7 @@ const TrendingSearchPillContainer = styled.div`
`

const Avatar = styled(Avatar_)`
${(props) => getSize(props.theme as SearchTheme, true)}
${(props) => getSize(props.theme, true)}
`

const TrendingIcon = styled(TrendingIcon_)`
Expand Down
17 changes: 8 additions & 9 deletions packages/react-components/src/components/search-bar/theme.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { css } from '@emotion/core'
import styled, { CreateStyled } from '@emotion/styled'

export const mobileQuery = `max-width: 480px`

export type SearchTheme = {
mode: 'dark' | 'light'
// the height of the search bar on desktop
searchbarHeight: number
// the height of the search bar on mobile
smallSearchbarHeight: number
channelSearch: number
smallChannelSearch: number
}

const channelSearchSize = (size: number, margin = 6) => size - margin * 2
export const initTheme = (theme?: Partial<SearchTheme>): SearchTheme => {
const defaultTheme = {
return {
mode: 'light',
searchbarHeight: 42,
smallSearchbarHeight: 35,
...theme,
}
return {
...defaultTheme,
channelSearch: channelSearchSize(defaultTheme.searchbarHeight),
smallChannelSearch: channelSearchSize(defaultTheme.smallSearchbarHeight, 3),
}
}

// DRY but kinda ugly
Expand All @@ -38,3 +35,5 @@ export const getSize = (theme: SearchTheme, includeWidth: boolean = false) => cs
`};
}
`

export default styled as CreateStyled<SearchTheme>
12 changes: 5 additions & 7 deletions packages/react-components/stories/search-bar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,11 @@ const Components = () => {
)
}

export const SearchExperience = () => {
return (
<SearchContextManager apiKey={apiKey}>
<Components />
</SearchContextManager>
)
}
export const SearchExperience = () => (
<SearchContextManager apiKey={apiKey}>
<Components />
</SearchContextManager>
)

export const SearchExperienceInitialTerm = () => {
return (
Expand Down

0 comments on commit f9cf7ca

Please sign in to comment.