Merge pull request #134 from acid-info/search-fixes

Search fixes
This commit is contained in:
amir houieh 2023-08-28 18:50:04 +02:00 committed by GitHub
commit 4a5a093a19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 351 additions and 235 deletions

View File

@ -46,6 +46,7 @@
"graphql": "^16.7.1",
"graphql-request": "^6.0.0",
"next": "13.3.0",
"next-query-params": "^4.2.3",
"nextjs-progressbar": "^0.0.16",
"react": "18.2.0",
"react-blurhash": "^0.3.0",
@ -53,7 +54,8 @@
"react-imgix": "^9.7.0",
"react-player": "^2.12.0",
"react-use": "^17.4.0",
"typescript": "5.0.4"
"typescript": "5.0.4",
"use-query-params": "^2.2.1"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",

View File

@ -3,6 +3,7 @@ import styled from '@emotion/styled'
import { PostCard } from '@/components/PostCard'
import { SearchResultTopPostBlocks } from '@/components/Search/SearchResult.TopPost.Blocks'
import { Typography } from '@acid-info/lsd-react'
import { lsdUtils } from '@/utils/lsd.utils'
interface Props {
post: LPE.Search.ResultItemBase<LPE.Post.Document>
@ -29,4 +30,8 @@ export const SearchResultTopPost = ({ post, shows, blocks }: Props) => {
const Container = styled.div`
padding: 24px 0;
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
padding: 0;
}
`

View File

@ -1,40 +1,29 @@
import styled from '@emotion/styled'
import styles from '@/components/Searchbar/Search.module.css'
import {
Button,
Dropdown,
DropdownItem,
DropdownMenu,
TabItem,
Tabs,
TextField,
Typography,
} from '@acid-info/lsd-react'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
addContentTypesToQuery,
addQueryToQuery,
addTopicsToQuery,
extractContentTypesFromQuery,
extractQueryFromQuery,
extractTopicsFromQuery,
} from '@/utils/search.utils'
import { NextRouter, useRouter } from 'next/router'
import { Dropdown, TabItem, Tabs, Typography } from '@acid-info/lsd-react'
import React, { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/router'
import { nope } from '@/utils/general.utils'
import { LPE } from '@/types/lpe.types'
import PostTypes = LPE.PostTypes
import ContentBlockTypes = LPE.Post.ContentBlockTypes
import { ESearchScope } from '@/types/ui.types'
import { copyConfigs } from '@/configs/copy.configs'
import { useOutsideClick, useSticky } from '@/utils/ui.utils'
import { uiConfigs } from '@/configs/ui.configs'
import { useHydrated } from '@/utils/useHydrated.util'
import {
ArrayParam,
StringParam,
useQueryParam,
withDefault,
} from 'use-query-params'
import { useDeepCompareEffect } from 'react-use'
import { lsdUtils } from '@/utils/lsd.utils'
interface SearchBoxProps {
onSearch?: (query: string, tags: string[], types: LPE.ContentType[]) => void
tags?: string[]
onViewChange?: (view: string) => void
resultsNumber: number | null
busy?: boolean
showModeSwitch?: boolean
}
const ContentTypesCategories = {
@ -67,83 +56,6 @@ const contentTypes = [
const allContentTypes = contentTypes.map((c) => c.value)
const useSearchBox = (
router: NextRouter,
callback: (query: string, tags: string[], types: LPE.ContentType[]) => void,
) => {
const [query, setQuery] = useState<string>(
extractQueryFromQuery(router.query),
)
const [filterTags, setFilterTags] = useState<string[]>(
extractTopicsFromQuery(router.query),
)
const [filterContentTypes, setFilterContentTypes] =
useState<string[]>(allContentTypes)
useEffect(() => {
setQuery(extractQueryFromQuery(router.query))
setFilterTags(extractTopicsFromQuery(router.query))
const contentTypes = extractContentTypesFromQuery(router.query)
setFilterContentTypes(contentTypes.length ? contentTypes : allContentTypes)
}, [router.query, router.query.topics, router.pathname])
const performSearch = useCallback(
async (
q: string = query,
_filterTags: string[] = filterTags,
_contentTypes: string[] = [],
) => {
const queries = [
addQueryToQuery(q),
addTopicsToQuery(_filterTags),
addContentTypesToQuery(
_contentTypes.length === allContentTypes.length ? [] : _contentTypes,
),
].filter((n) => n && n)
callback(q, _filterTags, _contentTypes as LPE.ContentType[])
await router.push(
{
pathname: '/search',
query: queries.length ? queries.join('&') : undefined,
},
undefined,
{
shallow: true,
},
)
},
[router, filterTags, filterContentTypes, query],
)
const handleEnter = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
await performSearch()
}
}
const handleTypesChange = async (n: string | string[]) => {
await performSearch(undefined, undefined, n as string[])
}
const handleTagsChange = async (n: string | string[]) => {
await performSearch(undefined, n as string[])
}
return {
query,
filterTags,
filterContentTypes,
setQuery,
setFilterTags,
setFilterContentTypes,
handleEnter,
handleTypesChange,
handleTagsChange,
performSearch,
}
}
const SearchBox = (props: SearchBoxProps) => {
const {
onSearch = nope,
@ -151,20 +63,30 @@ const SearchBox = (props: SearchBoxProps) => {
tags = [],
resultsNumber,
busy = false,
showModeSwitch = true,
} = props
const router = useRouter()
const {
query,
filterTags,
filterContentTypes,
setQuery,
handleEnter,
handleTypesChange,
handleTagsChange,
performSearch,
} = useSearchBox(router, onSearch)
const [view, setView] = useState<string>('list')
const [filterTags, setFilterTags] = useQueryParam(
'topic',
withDefault(ArrayParam, []),
{
skipUpdateWhenNoChange: false,
},
)
const [filterContentTypes, setFilterContentTypes] = useQueryParam(
'type',
withDefault(ArrayParam, allContentTypes),
{
skipUpdateWhenNoChange: true,
},
)
const [query, setQuery] = useQueryParam('q', withDefault(StringParam, ''), {
skipUpdateWhenNoChange: true,
})
const [queryInput, setQueryInput] = useState<string>(query)
const [view, setView] = useState<string>('list')
const [enlargeQuery, setEnlargeQuery] = useState(false)
const [placeholder, setPlaceholder] = useState<string>(
copyConfigs.search.searchbarPlaceholders.global(),
@ -185,18 +107,24 @@ const SearchBox = (props: SearchBoxProps) => {
const handleViewChange = async (n: string) => {
setView(n)
onViewChange(n)
performSearch()
onSearch(
query,
filterTags as string[],
filterContentTypes as LPE.ContentType[],
)
}
useEffect(() => {
if (enlargeQuery) {
setEnlargeQuery(!(queryInput.length === 0 && !focused))
if (focused) {
setPlaceholder('')
} else {
setTimeout(() => {
setPlaceholder(copyConfigs.search.searchbarPlaceholders.global())
}, 200)
setQuery('')
}
}, [enlargeQuery])
}, [focused, queryInput])
useEffect(() => {
if (filtersRef.current) {
@ -204,7 +132,7 @@ const SearchBox = (props: SearchBoxProps) => {
const parentT =
filtersRef.current.parentElement?.getBoundingClientRect().top || 0
const whereResultsStick =
-1 * (filtersB - parentT - uiConfigs.navbarRenderedHeight)
-1 * (filtersB - parentT - uiConfigs.navbarRenderedHeight + 2)
setWhereResultsStick(whereResultsStick)
setDetailsTop(filtersB + whereResultsStick)
}
@ -218,17 +146,6 @@ const SearchBox = (props: SearchBoxProps) => {
return () => window.removeEventListener('scroll', onScroll)
}, [detailsTop])
useEffect(() => {
if (query.length === 0 && mounted && !focused) {
setTimeout(() => {
setEnlargeQuery(false)
}, 200)
} else {
setEnlargeQuery(true)
setPlaceholder('')
}
}, [query])
useEffect(() => {
if (
filterContentTypes.length < allContentTypes.length ||
@ -240,10 +157,32 @@ const SearchBox = (props: SearchBoxProps) => {
}
}, [filterTags, filterContentTypes])
const handleClear = async () => {
await performSearch(undefined, [], [])
const clear = async () => {
setQuery('')
setFilterTags([])
setFilterContentTypes(allContentTypes)
}
const handleKeyUp = async (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
setQuery(queryInput)
}
}
useEffect(() => {
if (query?.length > 0) setQueryInput(query)
}, [query])
useDeepCompareEffect(() => {
onSearch(
query,
filterTags as string[],
filterContentTypes as LPE.ContentType[],
)
}, [query, filterTags, filterContentTypes])
const hydrated = useHydrated()
return (
<Container
className={enlargeQuery ? 'active' : ''}
@ -253,54 +192,51 @@ const SearchBox = (props: SearchBoxProps) => {
<input
className={`search-input`}
placeholder={placeholder}
value={query as string}
value={queryInput as string}
onFocus={() => {
if (query.length === 0) {
setEnlargeQuery(true)
}
setFocused(true)
}}
onKeyDown={handleEnter}
onChange={(e) => setQuery(e.target.value)}
onKeyUp={handleKeyUp}
onChange={(e) => setQueryInput(e.target.value)}
onBlurCapture={() => {
if (query.length === 0) {
setEnlargeQuery(false)
performSearch()
}
setFocused(false)
}}
/>
{showModeSwitch && (
<ViewButtons>
<Tabs size={'small'} activeTab={view} onChange={handleViewChange}>
<TabItem name={'list'}>{copyConfigs.search.views.default}</TabItem>
<TabItem name={'list'}>
{copyConfigs.search.views.default}
</TabItem>
<TabItem name={'explore'}>
{copyConfigs.search.views.explore}
</TabItem>
</Tabs>
</ViewButtons>
)}
</FirstRow>
<Filters ref={filtersRef}>
<Dropdown
size={'small'}
placeholder={'Content Type'}
options={contentTypes.map((c) => ({ name: c.label, value: c.value }))}
value={filterContentTypes}
onChange={handleTypesChange}
value={hydrated ? (filterContentTypes as string[]) : []}
onChange={(n) => setFilterContentTypes(n as string[])}
multi={true}
/>
<Dropdown
size={'small'}
placeholder={'Topics'}
options={tags.map((t) => ({ name: t, value: t }))}
value={filterTags}
onChange={handleTagsChange}
value={hydrated ? (filterTags as string[]) : []}
onChange={(n) => setFilterTags(n as string[])}
multi={true}
triggerLabel={'Topics'}
/>
<Clear
variant={'label2'}
className={`${showClear ? 'show' : ''}`}
onClick={handleClear}
onClick={clear}
>
Clear Filters
</Clear>
@ -352,7 +288,7 @@ const Container = styled.div`
flex-direction: column;
gap: 12px;
border-bottom: 1px solid rgba(var(--lsd-text-primary), 1);
padding: 8px 0 14px 14px;
padding: 8px 14px;
position: sticky;
z-index: 1;
@ -391,7 +327,6 @@ const Container = styled.div`
}
`
// align last item to the right and first item takes the rest of the space
const FirstRow = styled.div`
display: flex;
flex-direction: row;
@ -410,6 +345,12 @@ const Filters = styled.div`
flex-direction: row;
align-items: center;
gap: 12px;
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
.lsd-dropdown--small {
width: 135px;
}
}
`
const Results = styled.div`
width: 100%;
@ -420,6 +361,10 @@ const Results = styled.div`
overflow: hidden;
> span {
white-space: nowrap;
}
.dot {
width: 3px;
height: 3px;

View File

@ -5,4 +5,9 @@ export const uiConfigs = {
articleSectionMargin: 40,
maxContainerWidth: 1440,
articleRenderedMT: 45 * 2,
searchExplore: {
gridColumns: 16,
largeColumn: 4,
smallColumn: 2,
},
}

View File

@ -4,13 +4,76 @@ import { Grid, GridItem } from '@/components/Grid/Grid'
import { useMemo } from 'react'
import { PostCard } from '@/components/PostCard'
import { ImageBlock, TextBlock } from '@/components/ContentBlock'
import { uiConfigs } from '@/configs/ui.configs'
type Block = LPE.Search.ResultItemBase<LPE.Post.ContentBlock>
type Post = LPE.Search.ResultItemBase<LPE.Post.Document>
interface Props {
posts: LPE.Search.ResultItemBase<LPE.Post.Document>[]
blocks: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
posts: Post[]
blocks: Block[]
shows: LPE.Podcast.Show[]
}
const distributeBlocks = (blocks: (Block | Post)[]): (Block | Post)[][] => {
let rows: (Block | Post)[][] = []
let currentRow: (Block | Post)[] = []
let currentRowWidth = 0
const getBlockWidth = (block: Block | Post): number => {
return block.type === LPE.ContentTypes.Image
? uiConfigs.searchExplore.smallColumn
: uiConfigs.searchExplore.largeColumn
}
for (let i = 0; i < blocks.length; i++) {
const block = blocks[i]
const blockWidth = getBlockWidth(block)
if (currentRowWidth + blockWidth <= uiConfigs.searchExplore.gridColumns) {
currentRow.push(block)
currentRowWidth += blockWidth
} else {
// Look ahead to find a block that fits.
let swapIndex = -1
for (let j = i + 1; j < blocks.length; j++) {
const nextBlockWidth = getBlockWidth(blocks[j])
if (
currentRowWidth + nextBlockWidth <=
uiConfigs.searchExplore.gridColumns
) {
swapIndex = j
break
}
}
if (swapIndex !== -1) {
// Swap blocks and add the fitting block to the current row.
;[blocks[i], blocks[swapIndex]] = [blocks[swapIndex], blocks[i]]
currentRow.push(blocks[i])
currentRowWidth += blockWidth
} else {
// No fitting block found, go with the best fit.
rows.push([...currentRow])
currentRow = [block]
currentRowWidth = blockWidth
}
}
if (currentRowWidth === uiConfigs.searchExplore.gridColumns) {
rows.push([...currentRow])
currentRow = []
currentRowWidth = 0
}
}
if (currentRow.length) {
rows.push(currentRow)
}
return rows
}
export const SearchResultsExploreView = (props: Props) => {
const { posts, shows, blocks } = props
const results = useMemo(() => {
@ -18,17 +81,23 @@ export const SearchResultsExploreView = (props: Props) => {
return b.score - a.score
})
}, [posts, blocks])
const items = distributeBlocks(results)
return (
<Container cols={12}>
{results.map((result, index) => {
<Container>
{items.map((row, idx) => (
<Row key={`search-result-row-${idx}`} cols={12}>
{row.map((result, index) => {
return (
<ResultItem
key={index}
className={result.type === LPE.ContentTypes.Image ? 'w-2' : 'w-4'}
className={
result.type === LPE.ContentTypes.Image ? 'w-2' : 'w-4'
}
>
{(() => {
switch (result.type) {
case LPE.ContentTypes.Article:
case LPE.ContentTypes.Podcast:
return (
<PostCard
@ -40,6 +109,20 @@ export const SearchResultsExploreView = (props: Props) => {
contentType={result.type as LPE.PostType}
/>
)
case LPE.ContentTypes.Article:
return (
<PostCard
data={{
...PostCard.toData(
result.data as LPE.Post.Document,
shows,
),
coverImage: null,
}}
size={'medium'}
contentType={result.type as LPE.PostType}
/>
)
case LPE.ContentTypes.Image:
return (
<ImageBlock
@ -59,9 +142,27 @@ export const SearchResultsExploreView = (props: Props) => {
</ResultItem>
)
})}
</Row>
))}
</Container>
)
}
const Container = styled(Grid)``
const ResultItem = styled(GridItem)``
const Container = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 56px;
`
const Row = styled(Grid)`
grid-template-columns: repeat(${uiConfigs.searchExplore.gridColumns}, 1fr);
grid-column-gap: 16px;
`
const ResultItem = styled(GridItem)`
border-top: 1px solid rgb(var(--lsd-text-primary));
padding: 24px 0;
> * {
padding-top: 0;
}
`

View File

@ -7,20 +7,18 @@ import { SearchResultListPosts } from '@/components/Search/SearchResultList.Post
import { useMemo } from 'react'
import { SearchResultTopPost } from '@/components/Search/SearchResult.TopPost'
import { SearchResultListBlocks } from '@/components/Search/SearchResult.Blocks'
import { Typography } from '@acid-info/lsd-react'
import { lsdUtils } from '@/utils/lsd.utils'
interface Props {
posts: LPE.Search.ResultItemBase<LPE.Post.Document>[]
blocks: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
shows: LPE.Podcast.Show[]
}
type Accumulator = {
count: number
block: LPE.Search.ResultItemBase<LPE.Post.ContentBlock>
busy: boolean
}
export const SearchResultsListView = (props: Props) => {
const { posts, shows, blocks } = props
const { posts, shows, blocks, busy } = props
const mostReferredPostIndex = useMemo(() => {
// Extract the IDs of the first 3 posts
@ -90,25 +88,37 @@ export const SearchResultsListView = (props: Props) => {
</PostsListHeader>
)}
<PostsListContent>
{renderPosts.length > 0 && (
{renderPosts.length > 0 ? (
<>
<SearchResultsListHeader
title={copyConfigs.search.labels.articlesAndPodcasts}
/>
<SearchResultListPosts posts={renderPosts} shows={shows} />
</>
) : (
!busy && (
<Typography variant={'subtitle2'} genericFontFamily={'serif'}>
No results found
</Typography>
)
)}
</PostsListContent>
</PostsList>
<GridItem className={'w-1'} />
<BlocksList className={'w-3'}>
{renderBlocks.length > 0 && (
{renderBlocks.length > 0 ? (
<>
<SearchResultsListHeader
title={copyConfigs.search.labels.relatedContent}
/>
<SearchResultListBlocks blocks={renderBlocks} />
</>
) : (
!busy && (
<Typography variant={'subtitle2'} genericFontFamily={'serif'}>
No related content found
</Typography>
)
)}
</BlocksList>
</Container>
@ -117,12 +127,18 @@ export const SearchResultsListView = (props: Props) => {
const Container = styled(Grid)`
padding-top: 56px;
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
padding-top: 32px;
}
`
const PostsList = styled(GridItem)`
display: flex;
flex-direction: column;
gap: 56px;
${({ theme }) => lsdUtils.breakpoint(theme, 'xs', 'exact')} {
gap: 32px;
}
`
const PostsListHeader = styled.div``
const PostsListContent = styled.div``

View File

@ -2,11 +2,19 @@ import { SearchBox } from '@/components/SearchBox'
import { SearchResultsExploreView } from '@/containers/Search/ExploreView'
import { SearchResultsListView } from '@/containers/Search/ListView'
import { LPE } from '@/types/lpe.types'
import { useEffect, useState } from 'react'
import { ReactNode, useEffect, useState } from 'react'
import SEO from '../components/SEO/SEO'
import { DefaultLayout } from '../layouts/DefaultLayout'
import { api } from '../services/api.service'
import unbodyApi from '../services/unbody/unbody.service'
import { QueryParamProvider } from 'use-query-params'
import NextAdapterPages from 'next-query-params'
import { useQuery } from '@tanstack/react-query'
import { seq } from 'yaml/dist/schema/common/seq'
import { ApiResponse } from '@/types/data.types'
import useWindowSize from '@/utils/ui.utils'
import { uiConfigs } from '@/configs/ui.configs'
import themeState, { useThemeState } from '@/states/themeState/theme.state'
interface SearchPageProps {
topics: string[]
@ -18,15 +26,8 @@ interface SearchPageProps {
export default function SearchPage({ topics, shows }: SearchPageProps) {
const [mounted, setMounted] = useState(false)
const [busy, setBusy] = useState(false)
const [resultNumber, setResultNumber] = useState<number | null>(null)
const [view, setView] = useState<string>('list')
const [blocks, setBlocks] = useState<
LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
>([])
const [posts, setPosts] = useState<
LPE.Search.ResultItemBase<LPE.Post.Document>[]
>([])
const isMobile = useWindowSize().width < 768
useEffect(() => {
setMounted(true)
@ -35,24 +36,38 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
}
}, [])
const [query, setQuery] = useState<string>('')
const [tags, setTags] = useState<string[]>([])
const [types, setTypes] = useState<string[]>([])
const { data, isLoading } = useQuery(['search', query, tags, types], () => {
return api.search({
query: query.length > 0 ? query : ' ',
tags,
type: types as LPE.ContentType[],
})
})
const blocks = (data?.data.blocks ||
[]) as LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[]
const posts = (data?.data.posts ||
[]) as LPE.Search.ResultItemBase<LPE.Post.Document>[]
const handleSearch = async (
query: string,
filteredTags: string[],
filteredTypes: LPE.ContentType[],
) => {
setBusy(true)
const { data, errors } = await api.search({
query,
tags: filteredTags,
type: filteredTypes,
})
setBusy(false)
setResultNumber(data.posts.length || null)
setPosts(data.posts as LPE.Search.ResultItemBase<LPE.Post.Document>[])
setBlocks(data.blocks as LPE.Search.ResultItemBase<LPE.Post.ContentBlock>[])
console.log(data)
setQuery(query)
setTags(filteredTags)
setTypes(filteredTypes)
}
let resultsNumber =
types.includes(LPE.ContentTypes.Article) ||
types.includes(LPE.ContentTypes.Podcast)
? posts.length
: blocks.length
return (
<div style={{ minHeight: '80vh' }}>
<SEO
@ -64,12 +79,18 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
<SearchBox
tags={topics}
onSearch={handleSearch}
resultsNumber={resultNumber}
busy={busy}
resultsNumber={resultsNumber}
busy={isLoading}
onViewChange={setView}
showModeSwitch={!isMobile}
/>
{view === 'list' && (
<SearchResultsListView blocks={blocks} posts={posts} shows={shows} />
<SearchResultsListView
blocks={blocks}
posts={posts}
shows={shows}
busy={isLoading}
/>
)}
{view === 'explore' && (
<SearchResultsExploreView blocks={blocks} posts={posts} shows={shows} />
@ -78,9 +99,13 @@ export default function SearchPage({ topics, shows }: SearchPageProps) {
)
}
SearchPage.getLayout = (page: ReactNode) => (
<QueryParamProvider adapter={NextAdapterPages}>
<DefaultLayout>{page}</DefaultLayout>
</QueryParamProvider>
)
export async function getStaticProps() {
// const { data: articles = [] } = await unbodyApi.searchArticles()
// const { data: blocks = [] } = await unbodyApi.searchBlocks()
const { data: topics, errors: topicErrors } = await unbodyApi.getTopics()
const { data: shows = [] } = await unbodyApi.getPodcastShows({
populateEpisodes: true,
@ -89,8 +114,6 @@ export async function getStaticProps() {
return {
props: {
// articles,
// blocks: shuffle(blocks),
topics,
shows,
},

View File

@ -4165,6 +4165,13 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next-query-params@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/next-query-params/-/next-query-params-4.2.3.tgz#f90f1a56de8ac6e6a6adb492a11f8854cced314d"
integrity sha512-hGNCYRH8YyA5ItiBGSKrtMl21b2MAqfPkdI1mvwloNVqSU142IaGzqHN+OTovyeLIpQfonY01y7BAHb/UH4POg==
dependencies:
tslib "^2.0.3"
next@13.3.0:
version "13.3.0"
resolved "https://registry.yarnpkg.com/next/-/next-13.3.0.tgz#40632d303d74fc8521faa0a5bf4a033a392749b1"
@ -4963,6 +4970,11 @@ sentence-case@^3.0.4:
tslib "^2.0.3"
upper-case-first "^2.0.2"
serialize-query-params@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-2.0.2.tgz#598a3fb9e13f4ea1c1992fbd20231aa16b31db81"
integrity sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q==
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -5530,6 +5542,13 @@ urlpattern-polyfill@^9.0.0:
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-9.0.0.tgz#bc7e386bb12fd7898b58d1509df21d3c29ab3460"
integrity sha512-WHN8KDQblxd32odxeIgo83rdVDE2bvdkb86it7bMhYZwWKJz0+O0RK/eZiHYnM+zgt/U7hAHOlCQGfjjvSkw2g==
use-query-params@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-2.2.1.tgz#c558ab70706f319112fbccabf6867b9f904e947d"
integrity sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q==
dependencies:
serialize-query-params "^2.0.2"
use-sync-external-store@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"