diff --git a/packages/react-components/README.md b/packages/react-components/README.md
index ab2950ea..414a2c07 100644
--- a/packages/react-components/README.md
+++ b/packages/react-components/README.md
@@ -90,6 +90,9 @@ ReactDOM.render(, 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_ |
@@ -116,6 +119,92 @@ const { data } = await gf.gif('fpXxIjftmkk9y')
ReactDOM.render(, 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 = () => (
+
+
+
+)
+
+// 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 (
+ <>
+
+
+
+ >
+ )
+}
+```
+
+#### 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` | 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).
diff --git a/packages/react-components/src/components/search-bar/context.tsx b/packages/react-components/src/components/search-bar/context.tsx
index 6da6e0a3..f1c3c523 100644
--- a/packages/react-components/src/components/search-bar/context.tsx
+++ b/packages/react-components/src/components/search-bar/context.tsx
@@ -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[]
diff --git a/packages/react-components/src/components/search-bar/index.tsx b/packages/react-components/src/components/search-bar/index.tsx
index 0ee22749..7cd2e192 100644
--- a/packages/react-components/src/components/search-bar/index.tsx
+++ b/packages/react-components/src/components/search-bar/index.tsx
@@ -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(value: T) {
const ref = useRef(value)
@@ -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;
@@ -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;
diff --git a/packages/react-components/src/components/search-bar/search-bar-channel.tsx b/packages/react-components/src/components/search-bar/search-bar-channel.tsx
index ce1f011e..aaa980e1 100644
--- a/packages/react-components/src/components/search-bar/search-bar-channel.tsx
+++ b/packages/react-components/src/components/search-bar/search-bar-channel.tsx
@@ -1,14 +1,16 @@
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;
@@ -16,19 +18,18 @@ to {
`
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;
@@ -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;
}
diff --git a/packages/react-components/src/components/search-bar/search-button.tsx b/packages/react-components/src/components/search-bar/search-button.tsx
index 86ed8634..49b6354f 100644
--- a/packages/react-components/src/components/search-bar/search-button.tsx
+++ b/packages/react-components/src/components/search-bar/search-button.tsx
@@ -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'
@@ -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`
diff --git a/packages/react-components/src/components/search-bar/suggestion-bar/index.tsx b/packages/react-components/src/components/search-bar/suggestion-bar/index.tsx
index ec80dba5..0de0f61f 100644
--- a/packages/react-components/src/components/search-bar/suggestion-bar/index.tsx
+++ b/packages/react-components/src/components/search-bar/suggestion-bar/index.tsx
@@ -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`
@@ -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 = () => {
diff --git a/packages/react-components/src/components/search-bar/suggestion-bar/pills.tsx b/packages/react-components/src/components/search-bar/suggestion-bar/pills.tsx
index f6af8a3b..6c98cf18 100644
--- a/packages/react-components/src/components/search-bar/suggestion-bar/pills.tsx
+++ b/packages/react-components/src/components/search-bar/suggestion-bar/pills.tsx
@@ -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
@@ -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_)`
diff --git a/packages/react-components/src/components/search-bar/theme.ts b/packages/react-components/src/components/search-bar/theme.ts
index e32943da..c2d2d90f 100644
--- a/packages/react-components/src/components/search-bar/theme.ts
+++ b/packages/react-components/src/components/search-bar/theme.ts
@@ -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 => {
- 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
@@ -38,3 +35,5 @@ export const getSize = (theme: SearchTheme, includeWidth: boolean = false) => cs
`};
}
`
+
+export default styled as CreateStyled
diff --git a/packages/react-components/stories/search-bar.stories.tsx b/packages/react-components/stories/search-bar.stories.tsx
index 09bbd30a..fdfaf061 100644
--- a/packages/react-components/stories/search-bar.stories.tsx
+++ b/packages/react-components/stories/search-bar.stories.tsx
@@ -66,13 +66,11 @@ const Components = () => {
)
}
-export const SearchExperience = () => {
- return (
-
-
-
- )
-}
+export const SearchExperience = () => (
+
+
+
+)
export const SearchExperienceInitialTerm = () => {
return (