Skip to content

Commit

Permalink
added first implementation of facets
Browse files Browse the repository at this point in the history
  • Loading branch information
raffazizzi committed Oct 29, 2024
1 parent 0223f68 commit 81b353a
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 45 deletions.
7 changes: 5 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ import type { PropsWithChildren } from "react"
interface ButtonProps {
href?: string
style?: React.CSSProperties
color?: "default" | "transparent"
onClick?: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
}

const Button: React.FC<PropsWithChildren & ButtonProps> = ({href, children, style, onClick}) => (
<a href={href} className="mr-2 px-2 py-1 text-sm leading-normal rounded-[.2rem] text-gray-100 bg-rose-800 border-rose-800 hover:bg-rose-900 hover:border-rose-900 inline-block font-normal text-center align-middle cursor-pointer select-none border [transition:color_0.15s_ease-in-out,_background-color_0.15s_ease-in-out,_border-color_0.15s_ease-in-out,_box-shadow_0.15s_ease-in-out]"
const Button: React.FC<PropsWithChildren & ButtonProps> = ({href, color, children, style, onClick}) => (
<a href={href || "#"} className={`
${color !== "transparent" ? 'bg-rose-800 border-rose-800 hover:bg-rose-900 hover:border-rose-900' : ''}
mr-2 px-2 py-1 text-sm leading-normal rounded-[.2rem] text-gray-100 inline-block font-normal text-center align-middle cursor-pointer select-none border [transition:color_0.15s_ease-in-out,_background-color_0.15s_ease-in-out,_border-color_0.15s_ease-in-out,_box-shadow_0.15s_ease-in-out]`}
style={style}
onClick={onClick}
>{children}</a>
Expand Down
34 changes: 23 additions & 11 deletions src/components/FacetAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import type { PropsWithChildren } from "react"
interface FacetAccordionProps {
label: string
items: Item[]
fieldName: string
activeFacets: {cat: string, val: string}[]
add: (cat: string, val: string) => void
remove: (cat: string, val: string) => void
}

interface Item {
label: string
count: number
selected?: boolean
action: () => void
}

const FacetAccordion: React.FC<PropsWithChildren & FacetAccordionProps> = ({label, items}) => {
const FacetAccordion: React.FC<PropsWithChildren & FacetAccordionProps> = ({label, items, fieldName, activeFacets, add, remove}) => {
const [expanded, setExpanded] = React.useState(false)
const [expanding, setExpanding] = React.useState(false)
const [maxHeight, setMaxHeight] = React.useState<string | undefined>(undefined);
Expand Down Expand Up @@ -43,17 +45,23 @@ const FacetAccordion: React.FC<PropsWithChildren & FacetAccordionProps> = ({lab

const handleItemClick = (e: React.MouseEvent<HTMLAnchorElement>, item: Item) => {
e.preventDefault()
item.action()
add(fieldName, item.label)
}

const handleRemoveFacet = (e: React.MouseEvent<HTMLAnchorElement>, item: Item) => {
e.preventDefault()
remove(fieldName, item.label)
}

return (
<div className="w-full border rounded-md">
<button type="button" className="inline-flex w-full justify-between gap-x-1.5 bg-slate-100 px-3 py-2 text-sm font-semibold text-gray-900"
<div className="w-full border rounded-md mb-4">
<button type="button" className={`inline-flex w-full justify-between gap-x-1.5 px-3 py-2 text-sm font-semibold text-gray-900
${activeFacets.length > 0 ? 'bg-green-600' : 'bg-slate-100'}`}
aria-expanded={expanded ? 'true' : 'false'} aria-haspopup="true"
onClick={() => handleExpanded() }
>
{label}
<svg style={{transform: caretRotation}} className="-mr-1 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-slot="icon">
<svg style={{transform: caretRotation}} className="-mr-1 h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="#000" aria-hidden="true" data-slot="icon">
<path fillRule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clipRule="evenodd" />
</svg>
</button>
Expand All @@ -66,14 +74,18 @@ const FacetAccordion: React.FC<PropsWithChildren & FacetAccordionProps> = ({lab
>
{expanded &&
<ul className="table table-fixed w-full m-0 list-none p-4">{
items.map((item, _) => (
<li className="table-row">
items.map((item, _) => {
const active = Boolean(activeFacets.filter(f => f.val === item.label)[0])
return <li className={`table-row ${active ? 'text-green-600 font-bold' : ''}`} key={`v${item.label}`}>
<span className="table-cell px-4 -indent-4 pb-2 break-words hyphens-auto">
<a className="text-rose-800 hover:underline" href="#" onClick={(e) => handleItemClick(e, item)}>{item.label}</a>
{active
? <>{item.label}<a onClick={(e) => handleRemoveFacet(e, item)} href="#" className="text-gray-500 font-bold pl-2 text-[0.6rem] align-bottom hover:text-rose-800"><span aria-hidden="true"></span><span className="sr-only">[remove]</span></a></>
: <a className="text-rose-800 hover:underline" href="#" onClick={(e) => handleItemClick(e, item)}>{item.label}</a>
}
</span>
<span className="table-cell align-top text-right w-20">{item.count}</span>
</li>
))
})
}</ul>
}
</div>
Expand Down
137 changes: 105 additions & 32 deletions src/pages/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ const Results = ({results, start}: ResultProps) => (
<td>{d._xxxcollectionDescriptionxtxt}</td>
</tr>}
{d.scd_publish_status !== "collection-owner-title-description-only" && <>
<tr>
<td className="text-slate-500 text-right align-text-top">Content type{ctypes.length > 1 ? 's': ''}:</td>
<td>{ctypes.join("; ")}</td>
</tr>
{ctypes.length > 0 &&
<tr>
<td className="text-slate-500 text-right align-text-top">Content type{ctypes.length > 1 ? 's': ''}:</td>
<td>{ctypes.join("; ")}</td>
</tr>
}
{d._xcollectionFormatsxtxtxxxcollectionFormatsxtxt && <tr>
<td className="text-slate-500 text-right align-text-top">Format:</td>
<td>{d._xcollectionFormatsxtxtxxxcollectionFormatsxtxt}</td>
Expand All @@ -60,6 +62,7 @@ const Results = ({results, start}: ResultProps) => (
)

const SearchPage: React.FC<PageProps> = ({data}) => {
const [showFacets, setShowFacets] = React.useState(false)
const results = (data as Queries.qSearchPageQuery).allAirtableScdItems.nodes
const [currentPage, setCurrentPage] = React.useState(1)
const [resultsPerPage, setResultsPerPage] = React.useState<PerPageValues>(20)
Expand All @@ -68,16 +71,30 @@ const SearchPage: React.FC<PageProps> = ({data}) => {
const totalPages = Math.ceil(results.length / resultsPerPage)

const [sortOrder, setSortOrder] = React.useState<SortValues>("asc");
const [facets, setFacets] = React.useState<{cat: string, val: string}[]>([]);

// apply facets
const facetedResults = facets.length > 0 ? results.filter(r => {
for (const f of facets) {
const cat = f.cat as keyof Queries.qSearchPageQuery["allAirtableScdItems"]["nodes"][0]["data"]
if (Object.keys(r.data!).includes(cat)) {
if ((r.data![cat] as string[])?.includes(f.val)) {
return true
}
}
}
return false
}) : results;

// sort then paginate
(results as DeepWritable<Queries.qSearchPageQuery["allAirtableScdItems"]["nodes"]>).sort((a, b) => {
(facetedResults as DeepWritable<Queries.qSearchPageQuery["allAirtableScdItems"]["nodes"]>).sort((a, b) => {
if (sortOrder === "asc") {
return a.data!._xxxcollectionTitlextxt!.localeCompare(b.data!._xxxcollectionTitlextxt!)
} else {
return b.data!._xxxcollectionTitlextxt!.localeCompare(a.data!._xxxcollectionTitlextxt!)
}
})
const paginatedResults = results.slice(startIndex, endIndex)
const paginatedResults = facetedResults.slice(startIndex, endIndex)

// Update component with existing query parameters on load
React.useEffect(() => {
Expand Down Expand Up @@ -127,6 +144,16 @@ const SearchPage: React.FC<PageProps> = ({data}) => {
quietlyUpdateUrlSearch("sort", val.toString())
}

const handleAddFacet = (cat: string, val: string) => {
if (facets.filter(f => f.cat === cat && f.val === val)[0] === undefined) {
setFacets([...facets, {cat, val}])
}
}

const handleRemoveFacet = (cat: string, val: string) => {
setFacets(facets.filter(f => !(f.cat === cat && f.val === val)))
}

const SmallPagination = () => {
const prev = currentPage > 1 ? <><a href="#" onClick={(e) => handleChange(e, "prev")} className="hover:underline">« Previous</a> | </> : "";
const next = currentPage < totalPages ? <> | <a href="#" onClick={(e) => handleChange(e, "next")} className="hover:underline">Next »</a></> : "";
Expand All @@ -146,40 +173,86 @@ const SearchPage: React.FC<PageProps> = ({data}) => {
history.pushState(null, '', '?' + urlParams.toString());
}
}
return <div>{prev}<strong>{startIndex+1}{endIndex}</strong> of <strong>{results.length}</strong>{next}</div>
return <div>{prev}<strong>{startIndex+1}{endIndex}</strong> of <strong>{facetedResults.length}</strong>{next}</div>
}

// Get content types
const contentTypes = results.reduce((acc: { label: string; count: number, action: () => null }[], item) => {
// If contentTypes is not present, skip
if (!item.data?._xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt) return acc;

// Iterate through each contentType in the current item
item.data?._xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt.forEach(type => {
// Find if the contentType is already in the accumulator
const existing = acc.find(entry => entry.label === type);
if (existing) {
// If found, increment the count
existing.count += 1;
} else {
// If not found, add a new entry with count 1
acc.push({ label: type || "", count: 1, action: () => null });
}
});

return acc;
}, [])
.sort((a, b) => b.count - a.count);
// Function to extract facets
const extractFacet = (field: string) => {
const f = field as keyof Queries.qSearchPageQuery["allAirtableScdItems"]["nodes"][0]["data"]
return results.reduce((acc: { label: string; count: number, action: () => null }[], item) => {
// If facet value is not present, skip
if (!item.data![f]) return acc;
const values = Array.isArray(item.data![f]) ? item.data![f] : [item.data![f] as string]
// Iterate through each facet value in the current item
values.forEach(type => {
// Find if the facet value is already in the accumulator
const existing = acc.find(entry => entry.label === type);
if (existing) {
// If found, increment the count
existing.count += 1;
} else {
// If not found, add a new entry with count 1
acc.push({ label: type || "", count: 1, action: () => null });
}
});

return acc;
}, [])
.sort((a, b) => b.count - a.count);
}

return (
<Layout>
<div className="w-full max-w-hlg md:flex-nowrap md:justify-start justify-between items-center px-4 m-auto">
<div>
<div className=""></div>
<div className="flex">
<div className="flex-none w-1/4 px-2">
<h2 className="text-2xl mb-2 leading-tight h-14" id="search">Limit your search</h2>
<FacetAccordion label="Content type" items={contentTypes} />
<div className="lg:flex">
<div className="lg:flex-none lg:w-1/4 px-2 mb-6">
<div className="flex justify-between"><h2 className="text-2xl mb-2 leading-tight h-14" id="search">Limit your search</h2>
<span className="lg:hidden block">
<Button color="transparent" onClick={(e) => {e.preventDefault(); setShowFacets(!showFacets)}}>
<svg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'><path stroke='rgba(0, 0, 0, 0.5)' strokeLinecap='round' stroke-miterlimit='10' strokeWidth='2' d='M4 7h22M4 15h22M4 23h22'/></svg>
</Button>
</span>
</div>
<div className={`${showFacets ? '' : 'hidden'} lg:block`}>
<FacetAccordion
label="Content type" fieldName="_xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt"
items={extractFacet("_xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt")}
activeFacets={facets.filter(f => f.cat === "_xxxcollectionContentTypesxtxtxxxcollectionContentTypesxtxt")}
add={handleAddFacet}
remove={handleRemoveFacet}/>
<FacetAccordion
label="Format" fieldName="_xcollectionFormatsxtxtxxxcollectionFormatsxtxt"
items={extractFacet("_xcollectionFormatsxtxtxxxcollectionFormatsxtxt")}
activeFacets={facets.filter(f => f.cat === "_xcollectionFormatsxtxtxxxcollectionFormatsxtxt")}
add={handleAddFacet}
remove={handleRemoveFacet}/>
<FacetAccordion
label="Genre" fieldName="_xxxcollectionGenresxtxtxxxcollectionGenresxtxt"
items={extractFacet("_xxxcollectionGenresxtxtxxxcollectionGenresxtxt")}
activeFacets={facets.filter(f => f.cat === "_xxxcollectionGenresxtxtxxxcollectionGenresxtxt")}
add={handleAddFacet}
remove={handleRemoveFacet}/>
<FacetAccordion
label="Recpository/Collector" fieldName="_xxxcollectionOwnerNamextxt"
items={extractFacet("_xxxcollectionOwnerNamextxt")}
activeFacets={facets.filter(f => f.cat === "_xxxcollectionOwnerNamextxt")}
add={handleAddFacet}
remove={handleRemoveFacet}/>
<FacetAccordion
label="Country (Location)" fieldName="_xxxcollectionOwnerLocationCountryxtxt"
items={extractFacet("_xxxcollectionOwnerLocationCountryxtxt")}
activeFacets={facets.filter(f => f.cat === "_xxxcollectionOwnerLocationCountryxtxt")}
add={handleAddFacet}
remove={handleRemoveFacet}/>
<FacetAccordion
label="State (Location)" fieldName="_xxxcollectionOwnerLocationStatextxt"
items={extractFacet("_xxxcollectionOwnerLocationStatextxt")}
activeFacets={facets.filter(f => f.cat === "_xxxcollectionOwnerLocationStatextxt")}
add={handleAddFacet}
remove={handleRemoveFacet}/>
</div>
</div>
<div className="flex-1">
<div className="flex justify-between border-b border-slate-300 pb-4 h-14">
Expand Down

0 comments on commit 81b353a

Please sign in to comment.