Skip to content

Commit

Permalink
Add Search component
Browse files Browse the repository at this point in the history
  • Loading branch information
GuilleAngulo committed Jun 28, 2021
1 parent 62a57c5 commit d0a5d2d
Show file tree
Hide file tree
Showing 9 changed files with 901 additions and 12 deletions.
4 changes: 4 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
NEXT_PUBLIC_IMAGE_HOST=http://localhost:1337
NEXT_PUBLIC_GA_TRACKING=


NEXT_PUBLIC_MEILISEARCH_SERVER=http://localhost:7700
NEXT_PUBLIC_MEILISEARCH_KEY=
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"dependencies": {
"@apollo/client": "^3.3.20",
"@meilisearch/instant-meilisearch": "^0.5.0",
"@stripe/react-stripe-js": "^1.4.1",
"@stripe/stripe-js": "^1.15.1",
"@styled-icons/boxicons-regular": "^10.34.0",
Expand All @@ -57,6 +58,7 @@
"polished": "^4.1.3",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-instantsearch-dom": "^6.11.2",
"react-slick": "^0.28.1",
"storybook-addon-next-router": "^2.0.4",
"styled-components": "5.3.0",
Expand All @@ -78,6 +80,7 @@
"@types/jest": "^26.0.23",
"@types/node": "^15.12.4",
"@types/react": "^17.0.11",
"@types/react-instantsearch-dom": "^6.10.1",
"@types/react-slick": "^0.23.4",
"@types/styled-components": "^5.1.10",
"@typescript-eslint/eslint-plugin": "^4.28.0",
Expand Down
6 changes: 2 additions & 4 deletions src/components/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Link from 'next/link'

import { useState } from 'react'
import { Menu2 as MenuIcon } from '@styled-icons/remix-fill/Menu2'
import { Search as SearchIcon } from '@styled-icons/material-outlined/Search'
import { Close as CloseIcon } from '@styled-icons/material-outlined/Close'

import Button from 'components/Button'
Expand All @@ -12,6 +11,7 @@ import * as S from './styles'
import CartDropdown from 'components/CartDropdown'
import CartIcon from 'components/CartIcon'
import UserDropdown from 'components/UserDropdown'
import Search from 'components/Search'

export type MenuProps = {
username?: string | null
Expand Down Expand Up @@ -51,9 +51,7 @@ const Menu = ({ username, loading }: MenuProps) => {
{!loading && (
<>
<S.MenuGroup>
<S.IconWrapper>
<SearchIcon aria-label="Search" />
</S.IconWrapper>
<Search />
<S.IconWrapper>
<MediaMatch greaterThan="medium">
<CartDropdown />
Expand Down
214 changes: 214 additions & 0 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'

import { searchClient } from 'utils/meilisearchClient'
import {
InstantSearch,
Highlight,
Configure,
connectStateResults,
connectSearchBox,
connectHits
} from 'react-instantsearch-dom'
import { getImageUrl } from 'utils/getImageUrl'
import formatPrice from 'utils/format-price'

import { GameFragment_cover } from 'graphql/generated/GameFragment'

import {
Search as SearchIcon,
Close as CloseIcon
} from '@styled-icons/material-outlined'
import { Apple, Windows, Linux } from '@styled-icons/fa-brands'
import * as S from './styles'
import Button from 'components/Button'

const Search = () => {
const [isOpen, setIsOpen] = useState(false)

useEffect(() => {
document.body.style.overflow = isOpen ? 'hidden' : 'unset'

return () => {
document.body.style.overflow = 'unset'
}
}, [isOpen])

return (
<S.Wrapper isOpen={isOpen}>
<InstantSearch indexName="game" searchClient={searchClient}>
<SearchBox
handleVisibility={() => setIsOpen(!isOpen)}
isOpen={isOpen}
/>
{/** Maximum number of results */}
<Configure hitsPerPage={12} />
<Results />
</InstantSearch>
<S.Overlay onClick={() => setIsOpen(!isOpen)} aria-hidden={!isOpen} />
</S.Wrapper>
)
}

export default Search

export type SearchBoxProps = {
handleVisibility: () => void
isOpen: boolean
currentRefinement: string
refine: (v: string) => void
}

const SearchBox = connectSearchBox(
({ handleVisibility, isOpen, currentRefinement, refine }: SearchBoxProps) => {
const inputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
if (isOpen && inputRef.current) {
inputRef.current.focus()
}
}, [isOpen])

return (
<S.SearchForm role="search">
<S.InputWrapper isOpen={isOpen}>
<S.Input
type="search"
ref={inputRef}
placeholder="Search here…"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
value={currentRefinement}
onChange={(event) => refine(event.currentTarget.value)}
/>

<S.Icon>
{isOpen ? (
<CloseIcon onClick={handleVisibility} aria-label="Close Search" />
) : (
<SearchIcon onClick={handleVisibility} aria-label="Open Search" />
)}
</S.Icon>
</S.InputWrapper>
</S.SearchForm>
)
}
)

const Results = connectStateResults(({ searchState }) =>
searchState?.query ? <Hits /> : null
)

type Platform = {
name: 'windows' | 'linux' | 'mac'
}

export type GameHitProps = {
id: string
name: string
short_description: string
cover: GameFragment_cover | null
slug: string
price: number
platforms: Platform[]
release_date: string | null
}

export type HitsProps = {
hits: GameHitProps[]
}

const Hits = connectHits(({ hits }: HitsProps) => (
<S.List>
{hits.length ? (
hits.map((hit) => (
<S.ListItem key={hit.id}>
<Hit hit={hit} />
</S.ListItem>
))
) : (
<NoResults />
)}
</S.List>
))

export type HitProps = {
hit: GameHitProps
}

const Hit = ({ hit }: HitProps) => {
const platformIcons = {
linux: <Linux title="Linux" />,
mac: <Apple title="Mac" />,
windows: <Windows title="Windows" />
}
const releaseYear =
hit.release_date && new Date(hit.release_date).getFullYear()

return (
<Link href={`game/${hit.slug}`} passHref>
<S.Result>
<S.ImageWrapper>
<Image
src={`${getImageUrl(hit.cover?.url)}`}
alt={hit.name}
layout="fill"
objectFit="cover"
/>
</S.ImageWrapper>
<S.Info>
<S.Title>
<Highlight attribute="name" hit={hit} />
{releaseYear && (
<S.ReleaseYear
itemProp="releaseYear"
dateTime={hit.release_date!}
>
{releaseYear}
</S.ReleaseYear>
)}
</S.Title>
<S.Details>
<S.Price>{formatPrice(hit.price)}</S.Price>
<S.Platform>
{hit.platforms.map((icon: Platform) => (
<S.PlatformIcon key={`${hit.id}${icon.name}`}>
{platformIcons[icon.name]}
</S.PlatformIcon>
))}
</S.Platform>
</S.Details>
<S.Description>
<Highlight attribute="short_description" hit={hit} />
</S.Description>
</S.Info>
</S.Result>
</Link>
)
}

const NoResults = () => (
<S.NoResultsWrapper>
<Image
src="/img/empty.svg"
alt="A gamer in a couch playing videogame"
width={110}
height={110}
/>

<S.NoResultsInfo>
<S.NoResultsTitle>
<SearchIcon size={24} /> No Results Found
</S.NoResultsTitle>
<S.NoResultsDescription>
Try searching another term.
</S.NoResultsDescription>
<Link href="/" passHref>
<Button as="a">Explore all games </Button>
</Link>
</S.NoResultsInfo>
</S.NoResultsWrapper>
)
14 changes: 14 additions & 0 deletions src/components/Search/stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Story, Meta } from '@storybook/react/types-6-0'
import Search from '.'

export default {
title: 'Search',
component: Search,
parameters: {
backgrounds: {
default: 'won-dark'
}
}
} as Meta

export const Default: Story = () => <Search />
Loading

0 comments on commit d0a5d2d

Please sign in to comment.