feat: add module filter tabs

This commit is contained in:
Andrew Aquino
2026-06-24 01:54:06 +00:00
parent 144729b14f
commit 876e13de15
@@ -1,3 +1,4 @@
import { PackageIcon, SearchIcon } from "lucide-react";
import { type FC, type PropsWithChildren, useMemo, useState } from "react";
import { useQuery } from "react-query";
import { templateBuilderModules } from "#/api/queries/templateBuilder";
@@ -9,6 +10,8 @@ import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Loader } from "#/components/Loader/Loader";
import { SearchField } from "#/components/SearchField/SearchField";
import { Tabs, TabsList, TabsTrigger } from "#/components/Tabs/Tabs";
import { useKebabMenu } from "#/components/Tabs/utils/useKebabMenu";
import { useFuzzySearch } from "#/hooks/useFuzzySearch";
import { ModuleCard } from "./ModuleCard";
import {
@@ -72,8 +75,30 @@ export const ModuleSelectStep: FC<ModuleSelectStepProps> = ({
const [moduleSearchText, setModuleSearchText] = useState("");
const modules = data?.modules ?? [];
const [selectedFilterTab, setSelectedFilterTab] = useState("All");
// TODO category counts should update on search
const categoryCounts = new Map<string, number>();
for (const module of modules) {
categoryCounts.set(
module.category,
(categoryCounts.get(module.category) ?? 0) + 1,
);
}
const filterTabs: Array<{ value: string; count: number }> = [
{ value: "All", count: modules.length },
...categoryCounts.entries().map(([value, count]) => ({ value, count })),
];
const { containerRef, visibleTabs: visibleFilterTabs } = useKebabMenu({
tabs: filterTabs,
enabled: true,
isActive: true,
});
const searchedModules = useFuzzySearch({
allItems: modules,
allItems:
selectedFilterTab === "All"
? modules
: modules.filter((module) => module.category === selectedFilterTab),
searchText: moduleSearchText,
searchProperties: ["display_name", "description"],
});
@@ -154,18 +179,42 @@ export const ModuleSelectStep: FC<ModuleSelectStepProps> = ({
className="my-4"
/>
<Tabs
value={selectedFilterTab}
onValueChange={setSelectedFilterTab}
className="my-4"
>
<TabsList ref={containerRef}>
{visibleFilterTabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
<PackageIcon className="size-icon-sm" />
{tab.value} ({tab.count})
</TabsTrigger>
))}
</TabsList>
</Tabs>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{searchedModules.map((m) => (
<ModuleCard
key={m.id}
name={m.display_name}
description={m.description}
iconUrl={m.icon}
detailsUrl={moduleDetailsUrl(m.id)}
selected={selectedSet.has(m.id)}
onSelect={() => handleToggle(m)}
/>
))}
{searchedModules.length ? (
searchedModules.map((m) => (
<ModuleCard
key={m.id}
name={m.display_name}
description={m.description}
iconUrl={m.icon}
detailsUrl={moduleDetailsUrl(m.id)}
selected={selectedSet.has(m.id)}
onSelect={() => handleToggle(m)}
/>
))
) : (
<div className="col-span-full my-12 flex flex-col items-center gap-1 text-content-secondary">
<SearchIcon />
<p className="m-0 text-sm font-normal">
No module matched your search
</p>
</div>
)}
</div>
{conflicts.length > 0 && (