Commit c6b4cc8e authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: add phase0 searchbar (#4377)

* feat: add phase0 searchbar

* exhaustive deps

* use router Link'

* use correct navigate for tokens

* useLocation

* add util function for organizing search results

* fix mobile navbar link

* remove exhausted depedencies

* split suggestion rows to their own file

* add new file

* use pathname instead of hash

* use imageholder classname

* fallback update
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 819302b5
...@@ -11,6 +11,7 @@ import { ChainSwitcher } from './ChainSwitcher' ...@@ -11,6 +11,7 @@ import { ChainSwitcher } from './ChainSwitcher'
import { MenuDropdown } from './MenuDropdown' import { MenuDropdown } from './MenuDropdown'
import { MobileSideBar } from './MobileSidebar' import { MobileSideBar } from './MobileSidebar'
import * as styles from './Navbar.css' import * as styles from './Navbar.css'
import { SearchBar } from './SearchBar'
interface MenuItemProps { interface MenuItemProps {
href: string href: string
...@@ -45,7 +46,7 @@ const MobileNavbar = () => { ...@@ -45,7 +46,7 @@ const MobileNavbar = () => {
</Box> </Box>
<Box className={styles.rightSideMobileContainer}> <Box className={styles.rightSideMobileContainer}>
<Row gap="16"> <Row gap="16">
{/* TODO add Searchbar */} <SearchBar />
<MobileSideBar /> <MobileSideBar />
</Row> </Row>
</Box> </Box>
...@@ -92,7 +93,9 @@ const Navbar = () => { ...@@ -92,7 +93,9 @@ const Navbar = () => {
</MenuItem> </MenuItem>
</Row> </Row>
</Box> </Box>
<Box className={styles.middleContainer}>{/* TODO add Searchbar */}</Box> <Box className={styles.middleContainer}>
<SearchBar />
</Box>
<Box className={styles.rightSideContainer}> <Box className={styles.rightSideContainer}>
<Row gap="12"> <Row gap="12">
<MenuDropdown /> <MenuDropdown />
......
import { style } from '@vanilla-extract/css'
import { buttonTextSmall, subhead, subheadSmall } from 'nft/css/common.css'
import { breakpoints, sprinkles, vars } from '../../nft/css/sprinkles.css'
const DESKTOP_NAVBAR_WIDTH = '360px'
const baseSearchStyle = style([
sprinkles({
borderStyle: 'solid',
borderColor: 'lightGrayButton',
borderWidth: '1px',
paddingY: '12',
width: { mobile: 'viewWidth' },
}),
{
'@media': {
[`screen and (min-width: ${breakpoints.tabletSm}px)`]: {
width: DESKTOP_NAVBAR_WIDTH,
},
},
},
])
export const searchBar = style([
baseSearchStyle,
sprinkles({
height: 'full',
color: 'placeholder',
paddingX: '16',
cursor: 'pointer',
}),
])
export const searchBarInput = style([
sprinkles({
padding: '0',
fontWeight: 'normal',
fontSize: '16',
color: { default: 'blackBlue', placeholder: 'placeholder' },
border: 'none',
background: 'none',
}),
{ lineHeight: '24px' },
])
export const searchBarDropdown = style([
baseSearchStyle,
sprinkles({
position: 'absolute',
left: '0',
top: '48',
borderBottomLeftRadius: '12',
borderBottomRightRadius: '12',
background: 'white',
}),
{
borderTop: 'none',
},
])
export const suggestionRow = style([
sprinkles({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingY: '8',
paddingX: '16',
}),
{
':hover': {
cursor: 'pointer',
background: vars.color.lightGrayContainer,
},
textDecoration: 'none',
},
])
export const suggestionImage = sprinkles({
width: '36',
height: '36',
borderRadius: 'round',
marginRight: '8',
})
export const suggestionPrimaryContainer = style([
sprinkles({
alignItems: 'flex-start',
width: 'full',
}),
])
export const suggestionSecondaryContainer = sprinkles({
textAlign: 'right',
alignItems: 'flex-end',
})
export const primaryText = style([
subhead,
sprinkles({
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
color: 'blackBlue',
}),
{
lineHeight: '24px',
},
])
export const secondaryText = style([
buttonTextSmall,
sprinkles({
color: 'darkGray',
}),
{
lineHeight: '20px',
},
])
export const imageHolder = style([
suggestionImage,
sprinkles({
background: 'loading',
flexShrink: '0',
}),
])
export const suggestionIcon = sprinkles({
display: 'flex',
flexShrink: '0',
})
export const magnifyingGlassIcon = style([
sprinkles({
width: '20',
height: '20',
marginRight: '12',
}),
])
export const sectionHeader = style([
subheadSmall,
sprinkles({
color: 'darkGray',
}),
{
lineHeight: '20px',
},
])
export const notFoundContainer = style([
sectionHeader,
sprinkles({
paddingY: '4',
paddingLeft: '16',
marginTop: '20',
}),
])
This diff is collapsed.
import clsx from 'clsx'
import uriToHttp from 'lib/utils/uriToHttp'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { vars } from 'nft/css/sprinkles.css'
import { useSearchHistory } from 'nft/hooks'
// import { fetchSearchCollections, fetchTrendingCollections } from 'nft/queries'
import { FungibleToken, GenieCollection } from 'nft/types'
import { ethNumberStandardFormatter } from 'nft/utils/currency'
import { putCommas } from 'nft/utils/putCommas'
import { useCallback, useEffect, useReducer, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { TokenWarningRedIcon, VerifiedIcon } from '../../nft/components/icons'
import * as styles from './SearchBar.css'
interface CollectionRowProps {
collection: GenieCollection
isHovered: boolean
setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void
index: number
}
export const CollectionRow = ({ collection, isHovered, setHoveredIndex, toggleOpen, index }: CollectionRowProps) => {
const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(collection)
toggleOpen()
}, [addToSearchHistory, collection, toggleOpen])
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isHovered) {
event.preventDefault()
navigate(`/nfts/collection/${collection.address}`)
handleClick()
}
}
document.addEventListener('keydown', keyDownHandler)
return () => {
document.removeEventListener('keydown', keyDownHandler)
}
}, [toggleOpen, isHovered, collection, navigate, handleClick])
return (
<Link
to={`/nfts/collection/${collection.address}`}
onClick={handleClick}
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
onMouseLeave={() => isHovered && setHoveredIndex(undefined)}
className={styles.suggestionRow}
style={{ background: isHovered ? vars.color.lightGrayButton : 'none' }}
>
<Row style={{ width: '68%' }}>
{!brokenImage && collection.imageUrl ? (
<Box
as="img"
src={collection.imageUrl}
alt={collection.name}
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
onError={() => setBrokenImage(true)}
onLoad={() => setLoaded(true)}
/>
) : (
<Box className={styles.imageHolder} />
)}
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
</Column>
</Row>
{collection.floorPrice && (
<Column className={styles.suggestionSecondaryContainer}>
<Row gap="4">
<Box className={styles.primaryText}>{ethNumberStandardFormatter(collection.floorPrice)} ETH</Box>
</Row>
<Box className={styles.secondaryText}>Floor</Box>
</Column>
)}
</Link>
)
}
interface TokenRowProps {
token: FungibleToken
isHovered: boolean
setHoveredIndex: (index: number | undefined) => void
toggleOpen: () => void
index: number
}
export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index }: TokenRowProps) => {
const [brokenImage, setBrokenImage] = useState(false)
const [loaded, setLoaded] = useState(false)
const addToSearchHistory = useSearchHistory(
(state: { addItem: (item: FungibleToken | GenieCollection) => void }) => state.addItem
)
const navigate = useNavigate()
const handleClick = useCallback(() => {
addToSearchHistory(token)
toggleOpen()
}, [addToSearchHistory, toggleOpen, token])
// Close the modal on escape
useEffect(() => {
const keyDownHandler = (event: KeyboardEvent) => {
if (event.key === 'Enter' && isHovered) {
event.preventDefault()
navigate(`/tokens/${token.address}`)
handleClick()
}
}
document.addEventListener('keydown', keyDownHandler)
return () => {
document.removeEventListener('keydown', keyDownHandler)
}
}, [toggleOpen, isHovered, token, navigate, handleClick])
return (
<Link
// TODO connect with explore token URI
to={`/tokens/${token.address}`}
onClick={handleClick}
onMouseEnter={() => !isHovered && setHoveredIndex(index)}
onMouseLeave={() => isHovered && setHoveredIndex(undefined)}
className={styles.suggestionRow}
style={{ background: isHovered ? vars.color.lightGrayButton : 'none' }}
>
<Row>
{!brokenImage && token.logoURI ? (
<Box
as="img"
src={token.logoURI.includes('ipfs://') ? uriToHttp(token.logoURI)[0] : token.logoURI}
alt={token.name}
className={clsx(loaded ? styles.suggestionImage : styles.imageHolder)}
onError={() => setBrokenImage(true)}
onLoad={() => setLoaded(true)}
/>
) : (
<Box className={styles.imageHolder} />
)}
<Column className={styles.suggestionPrimaryContainer}>
<Row gap="4" width="full">
<Box className={styles.primaryText}>{token.name}</Box>
{token.onDefaultList ? (
<VerifiedIcon className={styles.suggestionIcon} />
) : (
<TokenWarningRedIcon className={styles.suggestionIcon} />
)}
</Row>
<Box className={styles.secondaryText}>{token.symbol}</Box>
</Column>
</Row>
<Column className={styles.suggestionSecondaryContainer}>
{token.priceUsd && (
<Row gap="4">
<Box className={styles.primaryText}>{ethNumberStandardFormatter(token.priceUsd, true)}</Box>
</Row>
)}
{token.price24hChange && (
<Box className={styles.secondaryText} color={token.price24hChange >= 0 ? 'green400' : 'red400'}>
{token.price24hChange.toFixed(2)}%
</Box>
)}
</Column>
</Link>
)
}
export const SkeletonRow = () => {
const [isHovered, toggleHovered] = useReducer((s) => !s, false)
return (
<Box className={styles.searchBarDropdown}>
<Row
background={isHovered ? 'lightGrayButton' : 'none'}
onMouseEnter={toggleHovered}
onMouseLeave={toggleHovered}
className={styles.suggestionRow}
>
<Row>
<Box className={styles.imageHolder} />
<Box borderRadius="round" height="16" width="160" background="loading" />
</Row>
</Row>
</Box>
)
}
import { FungibleToken, GenieCollection } from 'nft/types'
/**
* Organizes the number of Token and NFT results to be shown to a user depending on if they're in the NFT or Token experience
* If not an nft page show up to 5 tokens, else up to 3. Max total suggestions of 8
* @param isNFTPage boolean if user is currently on an nft page
* @param tokenResults array of FungibleToken results
* @param collectionResults array of NFT Collection results
* @returns an array of Fungible Tokens and an array of NFT Collections with correct number of results to be shown
*/
export function organizeSearchResults(
isNFTPage: boolean,
tokenResults: FungibleToken[],
collectionResults: GenieCollection[]
): [FungibleToken[], GenieCollection[]] {
const reducedTokens =
tokenResults?.slice(0, isNFTPage ? 3 : collectionResults.length < 3 ? 8 - collectionResults.length : 5) ?? []
const reducedCollections = collectionResults.slice(0, 8 - reducedTokens.length)
return [reducedTokens, reducedCollections]
}
...@@ -6,15 +6,15 @@ export const overlay = style([ ...@@ -6,15 +6,15 @@ export const overlay = style([
sprinkles({ sprinkles({
top: '0', top: '0',
left: '0', left: '0',
width: 'full', width: 'viewWidth',
height: 'full', height: 'viewHeight',
position: 'fixed', position: 'fixed',
display: 'block', display: 'block',
background: 'black', background: 'black',
zIndex: '3',
}), }),
{ {
opacity: 0.75, zIndex: 10,
opacity: 0.72,
overflow: 'hidden', overflow: 'hidden',
}, },
]) ])
...@@ -143,6 +143,7 @@ export const vars = createGlobalTheme(':root', { ...@@ -143,6 +143,7 @@ export const vars = createGlobalTheme(':root', {
transculent: '#7F7F7F', transculent: '#7F7F7F',
transparent: 'transparent', transparent: 'transparent',
none: 'none', none: 'none',
loading: '#7C85A24D',
// new uniswap colors: // new uniswap colors:
blue400: '#4C82FB', blue400: '#4C82FB',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment