-
Notifications
You must be signed in to change notification settings - Fork 330
feat(plugins): introduce Tags plugin #645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sarahdayan
merged 32 commits into
feat/plugin-tags/boilerplate
from
feat/plugin-tags/plugin
Sep 10, 2021
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
078ff15
feat: introduce Tags plugin
sarahdayan 71d3c52
fixup! feat: introduce Tags plugin
sarahdayan 9590c8f
chore: export Tag type
sarahdayan 42cc8ef
chore: use better icons
sarahdayan 719366f
fix: update types
sarahdayan 6fe07e2
fix: fix types
sarahdayan c531b11
style: lint
sarahdayan 787694f
build: update bundlesize
sarahdayan fd7a9ed
docs: add TSDoc
sarahdayan 8752e78
refactor: drop getTemplates
sarahdayan dc12cbb
fix: pass default object
sarahdayan e9fb0ec
chore: remove readonly
sarahdayan 2c47ebb
refactor: move noop to shared
sarahdayan 4613b84
feat(tags-plugin): add theme
sarahdayan e55adcb
build: accept class name prefix with "Plugin" suffix
sarahdayan dcda76d
style: lint
sarahdayan a1f859e
fix: some space below source
sarahdayan b921c1c
chore: temporarily build UMD on prepare
sarahdayan d5734db
revert: revert "chore: temporarily build UMD on prepare"
sarahdayan 41f68dc
fix: make generic type optional
sarahdayan b905db3
chore: export all Tags types
sarahdayan 43ac42b
refactor: rename toTags to mapToTags
sarahdayan 396ff09
chore: remove unused style
sarahdayan 56be24f
refactor: drop intermediary variable
sarahdayan b833e4d
feat: type context
sarahdayan 7549b7b
fix: fix types
sarahdayan d36b7b6
fix: fix annotation
sarahdayan 5b60b52
chore: rename file
sarahdayan 01404f7
fix: fix version
sarahdayan 403e4e6
test(plugin): test Tags plugin (#649)
sarahdayan 4f61df6
docs(examples): add Tags with hits example (#646)
sarahdayan 08b761d
fix: remove global type declaration
sarahdayan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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
Empty file.
This file contains hidden or 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,368 @@ | ||
/** @jsx h */ | ||
import { | ||
autocomplete, | ||
AutocompleteComponents, | ||
getAlgoliaResults, | ||
getAlgoliaFacets, | ||
} from '@algolia/autocomplete-js'; | ||
import { | ||
AutocompleteInsightsApi, | ||
createAlgoliaInsightsPlugin, | ||
} from '@algolia/autocomplete-plugin-algolia-insights'; | ||
import { createTagsPlugin, Tag } from '@algolia/autocomplete-plugin-tags'; | ||
import algoliasearch from 'algoliasearch'; | ||
import { h, Fragment, render } from 'preact'; | ||
import groupBy from 'ramda/src/groupBy'; | ||
import insightsClient from 'search-insights'; | ||
|
||
import '@algolia/autocomplete-theme-classic'; | ||
|
||
import { ProductHit, TagExtraData } from './types'; | ||
|
||
const appId = 'latency'; | ||
const apiKey = '6be0576ff61c053d5f9a3225e2a90f76'; | ||
const searchClient = algoliasearch(appId, apiKey); | ||
|
||
// @ts-expect-error type error in search-insights | ||
insightsClient('init', { appId, apiKey }); | ||
|
||
const algoliaInsightsPlugin = createAlgoliaInsightsPlugin({ insightsClient }); | ||
|
||
const tagsPlugin = createTagsPlugin<TagExtraData>({ | ||
getTagsSubscribers() { | ||
return [ | ||
{ | ||
sourceId: 'brands', | ||
getTag({ item }) { | ||
return item; | ||
}, | ||
}, | ||
{ | ||
sourceId: 'categories', | ||
getTag({ item }) { | ||
return item; | ||
}, | ||
}, | ||
]; | ||
}, | ||
transformSource() { | ||
return undefined; | ||
}, | ||
onChange({ tags }) { | ||
requestAnimationFrame(() => { | ||
const container = document.querySelector('.aa-InputWrapperPrefix'); | ||
const oldTagsContainer = document.querySelector('.aa-Tags'); | ||
|
||
const tagsContainer = document.createElement('div'); | ||
tagsContainer.classList.add('aa-Tags'); | ||
|
||
render( | ||
<div className="aa-TagsList"> | ||
{tags.map((tag) => ( | ||
<TagItem key={tag.label} {...tag} /> | ||
))} | ||
</div>, | ||
tagsContainer | ||
); | ||
|
||
if (oldTagsContainer) { | ||
container.replaceChild(tagsContainer, oldTagsContainer); | ||
} else { | ||
container.appendChild(tagsContainer); | ||
} | ||
}); | ||
}, | ||
}); | ||
|
||
type TagItemProps<TTag> = Tag<TTag>; | ||
|
||
function TagItem<TTag>({ label, remove }: TagItemProps<TTag>) { | ||
return ( | ||
<div className="aa-Tag"> | ||
<span className="aa-TagLabel">{label}</span> | ||
<button | ||
className="aa-TagRemoveButton" | ||
onClick={() => remove()} | ||
title="Remove this tag" | ||
> | ||
<svg | ||
fill="none" | ||
stroke="currentColor" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
strokeWidth={2} | ||
viewBox="0 0 24 24" | ||
> | ||
<path d="M18 6L6 18"></path> | ||
<path d="M6 6L18 18"></path> | ||
</svg> | ||
</button> | ||
</div> | ||
); | ||
} | ||
|
||
autocomplete<ProductHit | Tag<TagExtraData>>({ | ||
container: '#autocomplete', | ||
placeholder: 'Search', | ||
openOnFocus: true, | ||
plugins: [algoliaInsightsPlugin, tagsPlugin], | ||
detachedMediaQuery: 'none', | ||
getSources({ query, state }) { | ||
const tagsByFacet = groupBy<Tag<TagExtraData>>( | ||
(tag) => tag.facet, | ||
state.context.tagsPlugin.tags | ||
); | ||
|
||
return [ | ||
{ | ||
sourceId: 'brands', | ||
onSelect({ item, state, setQuery }) { | ||
if ( | ||
item.label.toLowerCase().includes(state.query.toLowerCase().trim()) | ||
) { | ||
setQuery(''); | ||
} | ||
}, | ||
getItems({ query }) { | ||
return getAlgoliaFacets({ | ||
searchClient, | ||
queries: [ | ||
{ | ||
indexName: 'instant_search', | ||
facet: 'brand', | ||
params: { | ||
facetQuery: query, | ||
maxFacetHits: 3, | ||
filters: mapToAlgoliaNegativeFilters( | ||
state.context.tagsPlugin.tags, | ||
['brand'] | ||
), | ||
}, | ||
}, | ||
], | ||
transformResponse({ facetHits }) { | ||
return facetHits[0].map((hit) => ({ ...hit, facet: 'brand' })); | ||
}, | ||
}); | ||
}, | ||
templates: { | ||
header() { | ||
return ( | ||
<Fragment> | ||
<span className="aa-SourceHeaderTitle">Brands</span> | ||
<div className="aa-SourceHeaderLine" /> | ||
</Fragment> | ||
); | ||
}, | ||
item({ item, components }) { | ||
return ( | ||
<div className="aa-ItemWrapper"> | ||
<div className="aa-ItemContent"> | ||
<div className="aa-ItemContentBody"> | ||
<div className="aa-ItemContentTitle"> | ||
Filter on{' '} | ||
<components.Highlight hit={item} attribute="label" /> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="aa-ItemActions"> | ||
<button | ||
className="aa-ItemActionButton aa-DesktopOnly aa-ActiveOnly" | ||
type="button" | ||
title={`Filter on ${item.label}`} | ||
> | ||
<svg | ||
viewBox="0 0 24 24" | ||
fill="none" | ||
stroke="currentColor" | ||
strokeWidth={2} | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
> | ||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" /> | ||
</svg> | ||
</button> | ||
</div> | ||
</div> | ||
); | ||
}, | ||
noResults() { | ||
return 'No brands for this query.'; | ||
}, | ||
}, | ||
}, | ||
{ | ||
sourceId: 'products', | ||
getItems() { | ||
return getAlgoliaResults<ProductHit>({ | ||
searchClient, | ||
queries: [ | ||
{ | ||
indexName: 'instant_search', | ||
query, | ||
params: { | ||
clickAnalytics: true, | ||
attributesToSnippet: ['name:10'], | ||
snippetEllipsisText: '…', | ||
filters: mapToAlgoliaFilters(tagsByFacet), | ||
}, | ||
}, | ||
], | ||
}); | ||
}, | ||
templates: { | ||
header() { | ||
return ( | ||
<Fragment> | ||
<span className="aa-SourceHeaderTitle">Products</span> | ||
<div className="aa-SourceHeaderLine" /> | ||
</Fragment> | ||
); | ||
}, | ||
item({ item, components }) { | ||
return ( | ||
<ProductItem | ||
hit={item} | ||
components={components} | ||
insights={state.context.algoliaInsightsPlugin.insights} | ||
/> | ||
); | ||
}, | ||
noResults() { | ||
return 'No products for this query.'; | ||
}, | ||
}, | ||
}, | ||
]; | ||
}, | ||
}); | ||
|
||
type ProductItemProps = { | ||
hit: ProductHit; | ||
insights: AutocompleteInsightsApi; | ||
components: AutocompleteComponents; | ||
}; | ||
|
||
function ProductItem({ hit, insights, components }: ProductItemProps) { | ||
return ( | ||
<a href={hit.url} className="aa-ItemLink"> | ||
<div className="aa-ItemContent"> | ||
<div className="aa-ItemIcon aa-ItemIcon--picture aa-ItemIcon--alignTop"> | ||
<img src={hit.image} alt={hit.name} width="40" height="40" /> | ||
</div> | ||
<div className="aa-ItemContentBody"> | ||
<div className="aa-ItemContentTitle"> | ||
<components.Snippet hit={hit} attribute="name" /> | ||
</div> | ||
<div className="aa-ItemContentDescription"> | ||
From <strong>{hit.brand}</strong> in{' '} | ||
<strong>{hit.categories[0]}</strong> | ||
</div> | ||
{hit.rating > 0 && ( | ||
<div className="aa-ItemContentDescription"> | ||
<div style={{ display: 'flex', gap: 1, color: '#ffc107' }}> | ||
{Array.from({ length: 5 }, (_value, index) => { | ||
const isFilled = hit.rating >= index + 1; | ||
|
||
return ( | ||
<svg | ||
key={index} | ||
width="16" | ||
height="16" | ||
viewBox="0 0 24 24" | ||
fill={isFilled ? 'currentColor' : 'none'} | ||
stroke="currentColor" | ||
strokeWidth="3" | ||
strokeLinecap="round" | ||
strokeLinejoin="round" | ||
> | ||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" /> | ||
</svg> | ||
); | ||
})} | ||
</div> | ||
</div> | ||
)} | ||
<div className="aa-ItemContentDescription" style={{ color: '#000' }}> | ||
<strong>${hit.price.toLocaleString()}</strong> | ||
</div> | ||
</div> | ||
</div> | ||
<div className="aa-ItemActions"> | ||
<button | ||
className="aa-ItemActionButton aa-DesktopOnly aa-ActiveOnly" | ||
type="button" | ||
title="Select" | ||
style={{ pointerEvents: 'none' }} | ||
> | ||
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"> | ||
<path d="M18.984 6.984h2.016v6h-15.188l3.609 3.609-1.406 1.406-6-6 6-6 1.406 1.406-3.609 3.609h13.172v-4.031z" /> | ||
</svg> | ||
</button> | ||
<button | ||
className="aa-ItemActionButton" | ||
type="button" | ||
title="Add to cart" | ||
onClick={(event) => { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
|
||
insights.convertedObjectIDsAfterSearch({ | ||
eventName: 'Added to cart', | ||
index: hit.__autocomplete_indexName, | ||
objectIDs: [hit.objectID], | ||
queryID: hit.__autocomplete_queryID, | ||
}); | ||
}} | ||
> | ||
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"> | ||
<path d="M19 5h-14l1.5-2h11zM21.794 5.392l-2.994-3.992c-0.196-0.261-0.494-0.399-0.8-0.4h-12c-0.326 0-0.616 0.156-0.8 0.4l-2.994 3.992c-0.043 0.056-0.081 0.117-0.111 0.182-0.065 0.137-0.096 0.283-0.095 0.426v14c0 0.828 0.337 1.58 0.879 2.121s1.293 0.879 2.121 0.879h14c0.828 0 1.58-0.337 2.121-0.879s0.879-1.293 0.879-2.121v-14c0-0.219-0.071-0.422-0.189-0.585-0.004-0.005-0.007-0.010-0.011-0.015zM4 7h16v13c0 0.276-0.111 0.525-0.293 0.707s-0.431 0.293-0.707 0.293h-14c-0.276 0-0.525-0.111-0.707-0.293s-0.293-0.431-0.293-0.707zM15 10c0 0.829-0.335 1.577-0.879 2.121s-1.292 0.879-2.121 0.879-1.577-0.335-2.121-0.879-0.879-1.292-0.879-2.121c0-0.552-0.448-1-1-1s-1 0.448-1 1c0 1.38 0.561 2.632 1.464 3.536s2.156 1.464 3.536 1.464 2.632-0.561 3.536-1.464 1.464-2.156 1.464-3.536c0-0.552-0.448-1-1-1s-1 0.448-1 1z" /> | ||
</svg> | ||
</button> | ||
</div> | ||
</a> | ||
); | ||
} | ||
|
||
const searchInput: HTMLInputElement = document.querySelector( | ||
'.aa-Autocomplete .aa-Input' | ||
); | ||
|
||
searchInput.addEventListener('keydown', (event) => { | ||
if ( | ||
event.key === 'Backspace' && | ||
searchInput.selectionStart === 0 && | ||
searchInput.selectionEnd === 0 | ||
) { | ||
const newTags = tagsPlugin.data.tags.slice(0, -1); | ||
tagsPlugin.data.setTags(newTags); | ||
} | ||
}); | ||
|
||
function mapToAlgoliaFilters( | ||
tagsByFacet: Record<string, Array<Tag<TagExtraData>>>, | ||
operator = 'AND' | ||
) { | ||
return Object.keys(tagsByFacet) | ||
.map((facet) => { | ||
return `(${tagsByFacet[facet] | ||
.map(({ label }) => `${facet}:"${label}"`) | ||
.join(' OR ')})`; | ||
}) | ||
.join(` ${operator} `); | ||
} | ||
|
||
function mapToAlgoliaNegativeFilters( | ||
tags: Array<Tag<TagExtraData>>, | ||
facetsToNegate: string[], | ||
operator = 'AND' | ||
) { | ||
return tags | ||
.map(({ label, facet }) => { | ||
const filter = `${facet}:"${label}"`; | ||
|
||
return facetsToNegate.includes(facet) && `NOT ${filter}`; | ||
}) | ||
.filter(Boolean) | ||
.join(` ${operator} `); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.