feat(fs.dev): Implemented search
This commit is contained in:
parent
8f165dbe6d
commit
2b021e4223
7 changed files with 99 additions and 54 deletions
|
@ -2,6 +2,9 @@ import { useState, useEffect } from 'react'
|
||||||
import Logo from 'shared/components/logos/freesewing.js'
|
import Logo from 'shared/components/logos/freesewing.js'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import ThemePicker from 'shared/components/theme-picker.js'
|
import ThemePicker from 'shared/components/theme-picker.js'
|
||||||
|
import CloseIcon from 'shared/components/icons/close.js'
|
||||||
|
import MenuIcon from 'shared/components/icons/menu.js'
|
||||||
|
import SearchIcon from 'shared/components/icons/search.js'
|
||||||
|
|
||||||
const Right = props => (
|
const Right = props => (
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
@ -14,7 +17,7 @@ const Left = props => (
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const Header = ({ app }) => {
|
const Header = ({ app, setSearch }) => {
|
||||||
|
|
||||||
const [prevScrollPos, setPrevScrollPos] = useState(0)
|
const [prevScrollPos, setPrevScrollPos] = useState(0)
|
||||||
const [show, setShow] = useState(true)
|
const [show, setShow] = useState(true)
|
||||||
|
@ -50,7 +53,7 @@ const Header = ({ app }) => {
|
||||||
<button
|
<button
|
||||||
className={`
|
className={`
|
||||||
btn btn-sm
|
btn btn-sm
|
||||||
text-base-100 bg-transparent
|
text-neutral-content bg-transparent
|
||||||
border border-transparent
|
border border-transparent
|
||||||
hover:bg-transparent hover:border-base-100
|
hover:bg-transparent hover:border-base-100
|
||||||
sm:hidden
|
sm:hidden
|
||||||
|
@ -58,11 +61,30 @@ const Header = ({ app }) => {
|
||||||
`}
|
`}
|
||||||
onClick={app.togglePrimaryMenu}>
|
onClick={app.togglePrimaryMenu}>
|
||||||
{app.primaryMenu
|
{app.primaryMenu
|
||||||
? <><Left /> Hide menu</>
|
? <><CloseIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Left />swipe</span></>
|
||||||
: <>Show menu <Right /></>
|
: <><MenuIcon /><span className="opacity-50 pl-2 flex flex-row items-center gap-1"><Right />swipe</span></>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2 sm:hidden">
|
||||||
|
<button className="btn btn-sm btn h-12 px-12" onClick={() => setSearch(true)}>
|
||||||
|
<SearchIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button className={`
|
||||||
|
btn btn-sm h-12
|
||||||
|
hidden sm:flex
|
||||||
|
flex-row gap-1 mr-4 w-64 px-2
|
||||||
|
bg-base-100 text-base-content
|
||||||
|
hover:bg-base-100 hover:text-base-content
|
||||||
|
justify-between
|
||||||
|
`} onClick={() => setSearch(true)}>
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<SearchIcon />
|
||||||
|
<span className="normal-case font-normal">Quick Search...</span>
|
||||||
|
</div>
|
||||||
|
<span className="normal-case">Ctrl K</span>
|
||||||
|
</button>
|
||||||
|
<div className="hidden md:flex md:flex-row gap-2">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<a className="flex flex-column items-center">
|
<a className="flex flex-column items-center">
|
||||||
<Logo size={36} fill="currentColor" stroke={false} />
|
<Logo size={36} fill="currentColor" stroke={false} />
|
||||||
|
@ -74,7 +96,7 @@ const Header = ({ app }) => {
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block">
|
<div className="hidden sm:flex flex-row items-center">
|
||||||
<ThemePicker app={app} />
|
<ThemePicker app={app} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -30,15 +30,15 @@ const Hits = props => {
|
||||||
|
|
||||||
const CustomHits = connectHits(Hits);
|
const CustomHits = connectHits(Hits);
|
||||||
|
|
||||||
const Highlight = ({ highlight, attribute, hit }) => {
|
const Highlight = ({ highlight, attribute, hit, snippet=false }) => {
|
||||||
const parsedHit = highlight({
|
const parsedHit = highlight({
|
||||||
highlightProperty: '_highlightResult',
|
highlightProperty: snippet ? '_snippetResult' : '_highlightResult',
|
||||||
attribute,
|
attribute,
|
||||||
hit,
|
hit,
|
||||||
});
|
});
|
||||||
|
|
||||||
return parsedHit.map((part, index) => part.isHighlighted
|
return parsedHit.map((part, index) => part.isHighlighted
|
||||||
? <mark key={index}>{part.value}</mark>
|
? <mark className="text-base-content bg-secondary-focus bg-opacity-30" key={index}>{part.value}</mark>
|
||||||
: <span key={index}>{part.value}</span>
|
: <span key={index}>{part.value}</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -48,34 +48,42 @@ const CustomHighlight = connectHighlight(Highlight);
|
||||||
const Hit = props => (
|
const Hit = props => (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
border-base-300
|
px-2 py-1 ounded mt-1
|
||||||
border px-3 py-1 rounded mt-1
|
text-base text-base-content
|
||||||
lg:px-6 lg:py-3 lg:border-2 lg:mt-4 lg:rounded-lg
|
sm:rounded
|
||||||
hover:bg-base-100 hover:text-base-content
|
lg:px-4 lg:py-2
|
||||||
|
hover:bg-secondary hover:bg-opacity-10 hover:text-base-content
|
||||||
${props.index === props.active
|
${props.index === props.active
|
||||||
? 'bg-base-300 text-base-content bg-opacity-30'
|
? 'bg-secondary bg-opacity-30'
|
||||||
: ''
|
: 'bg-base-300 bg-opacity-10'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Link href={props.hit.page}>
|
<Link href={props.hit.page}>
|
||||||
<a href={props.hit.page}>
|
<a href={props.hit.page} className="flex flex-row justify-between gap-2">
|
||||||
<h4 className="text-lg lg:text-2xl">
|
<span className="text-base sm:text-xl font-bold leading-5">
|
||||||
{props.hit?._highlightResult?.title
|
{props.hit?._highlightResult?.title
|
||||||
? <CustomHighlight hit={props.hit} attribute='title' />
|
? <CustomHighlight hit={props.hit} attribute='title' />
|
||||||
: props.hit.title
|
: props.hit.title
|
||||||
}
|
}
|
||||||
</h4>
|
</span>
|
||||||
<p className="text-sm lg:text-lg">
|
<span className="text-xs pt-0.5 sm:text-base sm:pt-1 font-bold uppercase">{props.hit.page.split('/')[1]}</span>
|
||||||
/
|
|
||||||
<b className="font-bold px-1 lg:text-lg">
|
|
||||||
{props.hit.page.split('/')[1]}
|
|
||||||
</b>
|
|
||||||
/
|
|
||||||
{props.hit.page.split('/').slice(2).join('/')}
|
|
||||||
</p>
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
{props.hit?._snippetResult?.body && (
|
||||||
|
<Link href={props.hit.page}>
|
||||||
|
<a href={props.hit.page} className="text-sm sm:text-base block py-1">
|
||||||
|
<CustomHighlight hit={props.hit} attribute='body' snippet />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{props.hit?._highlightResult?.page && (
|
||||||
|
<Link href={props.hit.page}>
|
||||||
|
<a href={props.hit.page} className="text-xs sm:text-sm block opacity-70">
|
||||||
|
<CustomHighlight hit={props.hit} attribute='page' />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -85,7 +93,6 @@ const handleInputKeydown = (evt, setSearch, setActive, active, router) => {
|
||||||
if (evt.key === 'Escape') setSearch(false)
|
if (evt.key === 'Escape') setSearch(false)
|
||||||
if (evt.key === 'ArrowDown') setActive(act => act + 1)
|
if (evt.key === 'ArrowDown') setActive(act => act + 1)
|
||||||
if (evt.key === 'ArrowUp') setActive(act => act - 1)
|
if (evt.key === 'ArrowUp') setActive(act => act - 1)
|
||||||
if (evt.key === 'c' && prev === 'Control') evt.target.value = ''
|
|
||||||
if (evt.key === 'Enter') {
|
if (evt.key === 'Enter') {
|
||||||
// Trigger navigation
|
// Trigger navigation
|
||||||
if (evt?.target?.dataset?.links) {
|
if (evt?.target?.dataset?.links) {
|
||||||
|
@ -107,20 +114,9 @@ const SearchBox = props => {
|
||||||
const { currentRefinement, isSearchStalled, refine, setSearch, setActive } = props
|
const { currentRefinement, isSearchStalled, refine, setSearch, setActive } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="py-8">
|
||||||
<form noValidate action="" role="search" onSubmit={(evt) => evt.preventDefault()}>
|
<form noValidate action="" role="search" onSubmit={(evt) => evt.preventDefault()}>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label hidden lg:block">
|
|
||||||
<span className="label-text">
|
|
||||||
<b> Escape</b> to exit
|
|
||||||
<span className="px-4">|</span>
|
|
||||||
<b> Up</b> or <b>Down</b> to select
|
|
||||||
<span className="px-4">|</span>
|
|
||||||
<b> Enter</b> to navigate
|
|
||||||
<span className="px-4">|</span>
|
|
||||||
<b> Ctrl+c</b> to clear
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
ref={input}
|
ref={input}
|
||||||
|
@ -129,17 +125,24 @@ const SearchBox = props => {
|
||||||
value={currentRefinement}
|
value={currentRefinement}
|
||||||
onChange={event => refine(event.currentTarget.value)}
|
onChange={event => refine(event.currentTarget.value)}
|
||||||
onKeyDown={(evt) => handleInputKeydown(evt, setSearch, setActive, props.active, router)}
|
onKeyDown={(evt) => handleInputKeydown(evt, setSearch, setActive, props.active, router)}
|
||||||
className="input lg:input-lg input-bordered input-primary w-full pr-16"
|
className="input lg:input-lg input-bordered input-neutral w-full pr-16"
|
||||||
placeholder='Type to search'
|
placeholder='Type to search'
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
className="absolute right-0 top-0 rounded-l-none btn btn-primary lg:btn-lg"
|
className="absolute right-0 top-0 rounded-l-none btn btn-neutral lg:btn-lg"
|
||||||
onClick={() => props.setSearch(false)}
|
onClick={() => props.setSearch(false)}
|
||||||
>X</button>
|
>X</button>
|
||||||
</div>
|
</div>
|
||||||
|
<label className="label hidden sm:block">
|
||||||
|
<div className="label-text flex flex-row gap-4 justify-between">
|
||||||
|
<div><b> Escape</b> to exit</div>
|
||||||
|
<div><b> Up</b> or <b>Down</b> to select</div>
|
||||||
|
<div><b> Enter</b> to navigate</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="overscroll-auto overflow-y-auto"
|
className="overscroll-auto overflow-y-auto mt-2"
|
||||||
style={{maxHeight: 'calc(100vh - 10rem)'}}
|
style={{maxHeight: 'calc(100vh - 10rem)'}}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
@ -192,14 +195,9 @@ const Search = props => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-prose m-auto">
|
<InstantSearch indexName={config.algolia.index} searchClient={searchClient}>
|
||||||
<h1>Search</h1>
|
<CustomSearchBox {...stateProps}/>
|
||||||
<InstantSearch indexName={config.algolia.index} searchClient={searchClient}>
|
</InstantSearch>
|
||||||
<div>
|
|
||||||
<CustomSearchBox {...stateProps}/>
|
|
||||||
</div>
|
|
||||||
</InstantSearch>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
8
packages/freesewing.shared/components/icons/close.js
Normal file
8
packages/freesewing.shared/components/icons/close.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/* Sourced from heroicons.com - Thanks guys! */
|
||||||
|
const Close = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Close
|
8
packages/freesewing.shared/components/icons/menu.js
Normal file
8
packages/freesewing.shared/components/icons/menu.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
/* Sourced from heroicons.com - Thanks guys! */
|
||||||
|
const Menu = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Menu
|
|
@ -76,7 +76,7 @@ const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
||||||
min-h-screen
|
min-h-screen
|
||||||
bg-base-100
|
bg-base-100
|
||||||
`}>
|
`}>
|
||||||
<Header app={app}/>
|
<Header app={app} setSearch={setSearch} />
|
||||||
<main className={`
|
<main className={`
|
||||||
grow flex flex-row
|
grow flex flex-row
|
||||||
gap-2
|
gap-2
|
||||||
|
@ -119,9 +119,18 @@ const DefaultLayout = ({ app, title=false, children=[], search, setSearch}) => {
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{search && (
|
{search && (
|
||||||
<div className={`fixed w-full min-h-screen bg-base-200 px-4 lg:py-24 top-0 z-20`}>
|
<>
|
||||||
|
<div className={`
|
||||||
|
fixed w-full max-h-screen bg-base-100 top-0 z-30 pt-0 pb-16 px-8
|
||||||
|
sm:rounded-lg sm:top-24
|
||||||
|
sm:max-w-xl sm:m-auto sm:inset-x-12
|
||||||
|
md:max-w-2xl
|
||||||
|
lg:max-w-4xl
|
||||||
|
`}>
|
||||||
<Search app={app} search={search} setSearch={setSearch}/>
|
<Search app={app} search={search} setSearch={setSearch}/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="fixed top-0 left-0 w-full min-h-screen bg-neutral z-20 bg-opacity-70"></div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Footer app={app} />
|
<Footer app={app} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -16,7 +16,7 @@ const Highlight = (props) => {
|
||||||
<div className={`text-xs uppercase font-bold text-neutral-content mt-1 text-center border-b border-neutral-content border-opacity-25 py-1 mb-2 lg:text-sm`}>
|
<div className={`text-xs uppercase font-bold text-neutral-content mt-1 text-center border-b border-neutral-content border-opacity-25 py-1 mb-2 lg:text-sm`}>
|
||||||
{names[language] ? names[language] : language}
|
{names[language] ? names[language] : language}
|
||||||
</div>
|
</div>
|
||||||
<pre className={`language-${language} hljs text-base lg:text-lg whitespace-pre-wrap`}>
|
<pre className={`language-${language} hljs text-base lg:text-lg whitespace-pre-wrap break-words`}>
|
||||||
{children}
|
{children}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -134,7 +134,7 @@ const SubLevel = ({ nodes={}, active }) => (
|
||||||
{child.__slug === active ? <>•</> : <>°</>}
|
{child.__slug === active ? <>•</> : <>°</>}
|
||||||
</span>
|
</span>
|
||||||
<span className={child.__slug === active ? 'font-bold' : ''}>
|
<span className={child.__slug === active ? 'font-bold' : ''}>
|
||||||
{child.__linktitle} here
|
{child.__linktitle}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue