-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
365 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { SearchPage as default } from '@/search/pages/search-page'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
'use client'; | ||
|
||
import Link from 'next/link'; | ||
|
||
import { Button } from '@nextui-org/react'; | ||
|
||
import { ExternalIcon } from '@/shared/components/icons/external-icon'; | ||
|
||
import { SearchResultDto } from '@/search/core/schemas'; | ||
|
||
const escapeRegExp = (str: string): string => | ||
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||
|
||
const highlightText = (text: string, query: string): React.ReactNode => { | ||
if (!query) return text; | ||
|
||
const escapedQuery = escapeRegExp(query); | ||
const regex = new RegExp(`(${escapedQuery})`, 'gi'); | ||
const parts = text.split(regex); | ||
|
||
return ( | ||
<div className=""> | ||
{parts.map((part, index) => | ||
regex.test(part) ? ( | ||
<span key={index} className="font-bold text-[#98eebe]"> | ||
{part} | ||
</span> | ||
) : ( | ||
<span key={index}>{part}</span> | ||
), | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
type Category = SearchResultDto['categories'][number]; | ||
|
||
interface Props extends Category { | ||
query: string; | ||
} | ||
|
||
export const SearchCategory = ({ label, url, query }: Props) => { | ||
return ( | ||
<Button | ||
as={Link} | ||
href={url} | ||
size="sm" | ||
className="" | ||
endContent={<ExternalIcon />} | ||
> | ||
{highlightText(label, query)} | ||
</Button> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
'use client'; | ||
|
||
import { Button, Input } from '@nextui-org/react'; | ||
|
||
import { CloseIcon } from '@/shared/components/icons/close-icon'; | ||
import { SearchIcon } from '@/shared/components/icons/sidebar-search-icon'; | ||
|
||
import { useSearchInput } from '@/search/hooks/use-search-input'; | ||
|
||
export const SearchInput = () => { | ||
const { value, onChange, onClear } = useSearchInput(); | ||
|
||
return ( | ||
<Input | ||
placeholder="Search ..." | ||
startContent={ | ||
<div className="shrink-0"> | ||
<SearchIcon /> | ||
</div> | ||
} | ||
endContent={ | ||
value ? ( | ||
<div className="flex items-center gap-2"> | ||
<span>Cancel</span> | ||
<Button isIconOnly size="sm" onClick={onClear}> | ||
<CloseIcon /> | ||
</Button> | ||
</div> | ||
) : null | ||
} | ||
value={value} | ||
onChange={onChange} | ||
/> | ||
); | ||
}; | ||
|
||
// 'use client'; | ||
|
||
// import { Button } from '@nextui-org/react'; | ||
|
||
// import { CloseIcon } from '@/shared/components/icons/close-icon'; | ||
// import { SearchIcon } from '@/shared/components/icons/sidebar-search-icon'; | ||
|
||
// import { useSearchInput } from '@/search/hooks/use-search-input'; | ||
|
||
// export const SearchInput = () => { | ||
// const { value, onChange } = useSearchInput(); | ||
|
||
// return ( | ||
// <div className="flex items-center gap-4 rounded-xl bg-white/5 p-2"> | ||
// <SearchIcon /> | ||
// <input | ||
// type="text" | ||
// placeholder="Search ..." | ||
// className="size-full grow bg-transparent text-white/90" | ||
// value={value} | ||
// onChange={onChange} | ||
// /> | ||
// <div className="flex items-center gap-2"> | ||
// <span>Cancel</span> | ||
// <Button isIconOnly size="sm"> | ||
// <CloseIcon /> | ||
// </Button> | ||
// </div> | ||
// </div> | ||
// ); | ||
// }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { SearchResultDto } from '@/search/core/schemas'; | ||
import { SearchCategory } from '@/search/components/search-category'; | ||
|
||
export const SearchResultLayout = ({ | ||
label, | ||
categories, | ||
}: { | ||
label: React.ReactNode; | ||
categories: React.ReactNode; | ||
}) => { | ||
return ( | ||
<div className="flex flex-col gap-4"> | ||
{label} | ||
<div className="flex flex-wrap gap-4">{categories}</div> | ||
</div> | ||
); | ||
}; | ||
|
||
interface Props extends SearchResultDto { | ||
query: string; | ||
} | ||
|
||
export const SearchResult = ({ query, title, categories }: Props) => { | ||
return ( | ||
<SearchResultLayout | ||
label={<span>{title}</span>} | ||
categories={ | ||
<> | ||
{categories.map(({ label, url }) => ( | ||
<SearchCategory key={label} query={query} label={label} url={url} /> | ||
))} | ||
</> | ||
} | ||
/> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Fragment } from 'react'; | ||
|
||
import { Skeleton } from '@nextui-org/react'; | ||
|
||
import { Divider } from '@/shared/components/divider'; | ||
|
||
import { SearchResultLayout } from '@/search/components/search-result'; | ||
|
||
export const SearchResultsSkeleton = () => { | ||
return ( | ||
<div className="flex flex-col gap-8"> | ||
{Array.from({ length: 5 }).map((_, i) => ( | ||
<Fragment key={i}> | ||
<Divider /> | ||
<SearchResultLayout | ||
label={<Skeleton className="h-5 w-20 rounded-md" />} | ||
categories={ | ||
<> | ||
{Array.from({ length: 5 }).map((_, i) => ( | ||
<Skeleton key={i} className="h-9 w-40 rounded-lg" /> | ||
))} | ||
</> | ||
} | ||
/> | ||
</Fragment> | ||
))} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
'use client'; | ||
|
||
import { Fragment } from 'react'; | ||
|
||
import { Divider } from '@/shared/components/divider'; | ||
|
||
import { useSearchResults } from '@/search/hooks/use-search-results'; | ||
import { SearchResult } from '@/search/components/search-result'; | ||
import { SearchResultsSkeleton } from '@/search/components/search-results-skeleton'; | ||
|
||
export const SearchResults = () => { | ||
const { query, data } = useSearchResults(); | ||
|
||
if (!data) return <SearchResultsSkeleton />; | ||
|
||
return ( | ||
<div className="flex flex-col gap-8"> | ||
{data.map(({ title: label, categories }) => ( | ||
<Fragment key={label}> | ||
<Divider /> | ||
<SearchResult query={query} title={label} categories={categories} /> | ||
</Fragment> | ||
))} | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { atom } from 'jotai'; | ||
|
||
export const searchQueryAtom = atom<string>(''); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export const searchQueryKeys = { | ||
all: ['search'] as const, | ||
search: (query: string) => [...searchQueryKeys.all, 'search', query] as const, | ||
}; | ||
|
||
export type SearchQueryKeys = typeof searchQueryKeys; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { z } from 'zod'; | ||
|
||
export const searchResultsDtoSchema = z.array( | ||
z.object({ | ||
title: z.string(), | ||
categories: z.array( | ||
z.object({ | ||
label: z.string(), | ||
url: z.string(), | ||
}), | ||
), | ||
}), | ||
); | ||
|
||
export type SearchResultsDto = z.infer<typeof searchResultsDtoSchema>; | ||
export type SearchResultDto = SearchResultsDto[number]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { fakeSearchResults } from '@/search/testutils/fake-search-results'; | ||
|
||
export const search = async (_query: string) => { | ||
await new Promise((r) => setTimeout(r, 200)); | ||
return fakeSearchResults(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { useAtom } from 'jotai'; | ||
|
||
import { searchQueryAtom } from '@/search/core/atoms'; | ||
|
||
export const useSearchInput = () => { | ||
const [value, setValue] = useAtom(searchQueryAtom); | ||
|
||
const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { | ||
setValue(e.target.value); | ||
}; | ||
|
||
const onClear = () => setValue(''); | ||
|
||
return { | ||
value, | ||
onChange, | ||
onClear, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
import { useAtomValue } from 'jotai'; | ||
|
||
import { QUERY_STALETIME } from '@/shared/core/constants'; | ||
|
||
import { searchQueryKeys } from '@/search/core/query-keys'; | ||
import { searchQueryAtom } from '@/search/core/atoms'; | ||
import { search } from '@/search/data/search'; | ||
|
||
export const useSearchResults = () => { | ||
const query = useAtomValue(searchQueryAtom); | ||
|
||
const fetchResult = useQuery({ | ||
// eslint-disable-next-line @tanstack/query/exhaustive-deps | ||
queryKey: searchQueryKeys.search(query.toLowerCase()), | ||
queryFn: () => search(query), | ||
staleTime: QUERY_STALETIME.DEFAULT, | ||
}); | ||
|
||
return { | ||
query, | ||
...fetchResult, | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { SearchInput } from '@/search/components/search-input'; | ||
import { SearchResults } from '@/search/components/search-results'; | ||
|
||
export const SearchPage = () => { | ||
return ( | ||
<div className="flex flex-col gap-8 p-8"> | ||
<SearchInput /> | ||
<SearchResults /> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { SearchResultsDto } from '@/search/core/schemas'; | ||
|
||
export const fakeSearchResults = (): SearchResultsDto => { | ||
return [ | ||
{ | ||
title: 'Jobs', | ||
categories: [ | ||
{ label: 'Creative Designer', url: '#' }, | ||
{ label: 'Staff Product Designer', url: '#' }, | ||
{ label: 'Design Customer Support', url: '#' }, | ||
{ label: 'Cyber Design', url: '#' }, | ||
{ label: 'Design Science', url: '#' }, | ||
{ label: 'Design Engineering', url: '#' }, | ||
], | ||
}, | ||
{ | ||
title: 'Organizations', | ||
categories: [ | ||
{ label: 'Business Design', url: '#' }, | ||
{ label: 'Golang Design', url: '#' }, | ||
{ label: 'Smart Design Contracts', url: '#' }, | ||
], | ||
}, | ||
{ | ||
title: 'Projects', | ||
categories: [ | ||
{ label: 'Analysis Design', url: '#' }, | ||
{ label: 'Project 1 Design', url: '#' }, | ||
{ label: 'Project 2 Design', url: '#' }, | ||
], | ||
}, | ||
{ | ||
title: 'Categories', | ||
categories: [ | ||
{ label: 'Design Category 1', url: '#' }, | ||
{ label: 'Design Category 2', url: '#' }, | ||
{ label: 'Design Category 3', url: '#' }, | ||
], | ||
}, | ||
{ | ||
title: 'Skills', | ||
categories: [ | ||
{ label: 'Design Skill 1', url: '#' }, | ||
{ label: 'Design Skill 2', url: '#' }, | ||
{ label: 'Design Skill 3', url: '#' }, | ||
], | ||
}, | ||
]; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { memo } from 'react'; | ||
|
||
export const CloseIcon = memo(() => ( | ||
<svg | ||
width="24" | ||
height="24" | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
xmlns="http://www.w3.org/2000/svg" | ||
> | ||
<path | ||
d="M16.192 6.34375L11.949 10.5858L7.70697 6.34375L6.29297 7.75775L10.535 11.9998L6.29297 16.2418L7.70697 17.6558L11.949 13.4137L16.192 17.6558L17.606 16.2418L13.364 11.9998L17.606 7.75775L16.192 6.34375Z" | ||
fill="white" | ||
/> | ||
</svg> | ||
)); | ||
|
||
CloseIcon.displayName = 'CloseIcon'; |
Oops, something went wrong.