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 (