
These are some changes in the way the monorepo is structured, that are aimed at making it easier to get started. There are two important changes: **Multiple workspaces** We had a yarn workspaces setup at `packages/*`. But our monorepo has grown to 92 packages which can be overwhelming for people not familiar with the package names. To remedy this, I've split it into 4 different workspaces: - `designs/*`: Holds FreeSewing designs (think patterns) - `plugins/*`: Holds FreeSewing plugins - `packages/*`: Holds other packages published on NPM - `sites/*`: Holds software that is not published as an NPM package, such as our various websites and backend API This should make it easier to find things, and to answer questions like *where do I find the code for the plugins*. **Updated reconfigure script to handle build order** One problem when bootstrapping the repo is inter-dependencies between packages. For example, building a pattern will only work once `plugin-bundle` is built. Which will only work once all plugins in the bundle or built. And that will only work when `core` is built, and so on. This can be frustrating for new users as `yarn buildall` will fail. And it gets overlooked by seasoned devs because they're likely to have every package built in their repo so this issue doesn't concern them. To remedy this, we now have a `config/build-order.mjs` file and the updated `/scripts/reconfigure.mjs` script will enforce the build order so that things *just work*.
204 lines
6.2 KiB
JavaScript
204 lines
6.2 KiB
JavaScript
import { useState, useRef } from 'react'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
|
|
import algoliasearch from 'algoliasearch/lite';
|
|
import { useHotkeys } from 'react-hotkeys-hook'
|
|
import { InstantSearch, connectHits, connectHighlight, connectSearchBox } from 'react-instantsearch-dom'
|
|
import config from 'site/freesewing.config.js'
|
|
|
|
const searchClient = algoliasearch(config.algolia.app, config.algolia.key)
|
|
|
|
const Hits = props => {
|
|
|
|
// When we hit enter in the text field, we want to navigate to the result
|
|
// which means we must make the result links available in the input somehow
|
|
// so let's stuff them in a data attribute
|
|
const links = props.hits.map(hit => hit.page)
|
|
props.input.current.setAttribute('data-links', JSON.stringify(links))
|
|
|
|
return props.hits.map((hit, index) => (
|
|
<Hit
|
|
key={hit.page}
|
|
{...props}
|
|
hit={hit}
|
|
index={index}
|
|
len={props.hits.length}
|
|
activeLink={links[props.active]}
|
|
/>
|
|
))
|
|
}
|
|
|
|
const CustomHits = connectHits(Hits);
|
|
|
|
const Highlight = ({ highlight, attribute, hit, snippet=false }) => {
|
|
const parsedHit = highlight({
|
|
highlightProperty: snippet ? '_snippetResult' : '_highlightResult',
|
|
attribute,
|
|
hit,
|
|
});
|
|
|
|
return parsedHit.map((part, index) => part.isHighlighted
|
|
? <mark className="text-base-content bg-secondary-focus bg-opacity-30" key={index}>{part.value}</mark>
|
|
: <span key={index}>{part.value}</span>
|
|
)
|
|
}
|
|
|
|
const CustomHighlight = connectHighlight(Highlight);
|
|
|
|
const Hit = props => (
|
|
<div
|
|
className={`
|
|
px-2 py-1 ounded mt-1
|
|
text-base text-base-content
|
|
sm:rounded
|
|
lg:px-4 lg:py-2
|
|
hover:bg-secondary hover:bg-opacity-10 hover:text-base-content
|
|
${props.index === props.active
|
|
? 'bg-secondary bg-opacity-30'
|
|
: 'bg-base-300 bg-opacity-10'
|
|
}
|
|
`}
|
|
>
|
|
<Link href={props.hit.page}>
|
|
<a href={props.hit.page} className="flex flex-row justify-between gap-2">
|
|
<span className="text-base sm:text-xl font-bold leading-5">
|
|
{props.hit._highlightResult?.title
|
|
? <CustomHighlight hit={props.hit} attribute='title' />
|
|
: props.hit.title
|
|
}
|
|
</span>
|
|
<span className="text-xs pt-0.5 sm:text-base sm:pt-1 font-bold uppercase">{props.hit.page.split('/')[1]}</span>
|
|
</a>
|
|
</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>
|
|
)
|
|
|
|
// We use this for trapping ctrl-c
|
|
const handleInputKeydown = (evt, setSearch, setActive, active, router) => {
|
|
if (evt.key === 'Escape') setSearch(false)
|
|
if (evt.key === 'ArrowDown') setActive(act => act + 1)
|
|
if (evt.key === 'ArrowUp') setActive(act => act - 1)
|
|
if (evt.key === 'Enter') {
|
|
// Trigger navigation
|
|
if (evt.target?.dataset?.links) {
|
|
router.push(JSON.parse(evt.target.dataset.links)[active])
|
|
setSearch(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
const SearchBox = props => {
|
|
|
|
const input = useRef(null)
|
|
const router = useRouter()
|
|
useHotkeys('ctrl+x', () => {
|
|
input.current.value = ''
|
|
})
|
|
if (input.current && props.active < 0) input.current.focus()
|
|
|
|
const { currentRefinement, refine, setSearch, setActive } = props
|
|
|
|
return (
|
|
<div className="py-8">
|
|
<form noValidate action="" role="search" onSubmit={(evt) => evt.preventDefault()}>
|
|
<div className="form-control">
|
|
<div className="relative">
|
|
<input
|
|
ref={input}
|
|
type="search"
|
|
autoFocus={true}
|
|
value={currentRefinement}
|
|
onChange={event => refine(event.currentTarget.value)}
|
|
onKeyDown={(evt) => handleInputKeydown(evt, setSearch, setActive, props.active, router)}
|
|
className="input lg:input-lg input-bordered input-neutral w-full pr-16"
|
|
placeholder='Type to search'
|
|
/>
|
|
<button
|
|
className="absolute right-0 top-0 rounded-l-none btn btn-neutral lg:btn-lg"
|
|
onClick={() => props.setSearch(false)}
|
|
>X</button>
|
|
</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
|
|
className="overscroll-auto overflow-y-auto mt-2"
|
|
style={{maxHeight: 'calc(100vh - 10rem)'}}
|
|
>
|
|
{
|
|
input.current
|
|
&& input.current.value.length > 0
|
|
&& <CustomHits hitComponent={Hit} {...props} input={input}/>
|
|
}
|
|
</div>
|
|
</form>
|
|
<div className={`
|
|
bg-neutral text-neutral-content
|
|
z-20 w-full mx-auto
|
|
lg:bg-base-100 lg:border-base-200
|
|
fixed bottom-0 left-0 border-t-2
|
|
lg:hidden
|
|
`}>
|
|
<div className='px-4 py-0 flex flex-row w-full lg:py-2'>
|
|
<button
|
|
className={`btn btn-ghost btn-block`}
|
|
onClick={() => props.setSearch(false)}
|
|
>
|
|
<span className='px-2 pt-2 pb-2'>Close Search</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const CustomSearchBox = connectSearchBox(SearchBox);
|
|
|
|
const Search = props => {
|
|
|
|
const [active, setActive] = useState(0)
|
|
useHotkeys('esc', () => props.setSearch(false))
|
|
useHotkeys('up', () => {
|
|
if (active) setActive(act => act - 1)
|
|
})
|
|
useHotkeys('down', () => {
|
|
setActive(act => act + 1)
|
|
})
|
|
useHotkeys('down', () => {
|
|
console.log('enter', active)
|
|
})
|
|
|
|
const stateProps = {
|
|
setSearch: props.setSearch,
|
|
setMenu: props.setMenu,
|
|
active, setActive
|
|
}
|
|
|
|
return (
|
|
<InstantSearch indexName={config.algolia.index} searchClient={searchClient}>
|
|
<CustomSearchBox {...stateProps}/>
|
|
</InstantSearch>
|
|
)
|
|
}
|
|
|
|
export default Search
|