Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/knowledgeBase/components/KbSearch.tsx
T

163 lines
6.7 KiB
TypeScript

import clsx from "clsx";
import { InfoIcon, SearchIcon, XIcon } from "~/components/ui/icons/vendor";
import ColorBackgroundUtils from "~/utils/shared/colors/ColorBackgroundUtils";
import ColorFocusUtils from "~/utils/shared/colors/ColorFocusUtils";
import ColorFocusWithinUtils from "~/utils/shared/colors/ColorFocusWithinUtils";
import ColorPlaceholderUtils from "~/utils/shared/colors/ColorPlaceholderUtils";
import ColorRingUtils from "~/utils/shared/colors/ColorRingUtils";
import ColorTextUtils from "~/utils/shared/colors/ColorTextUtils";
import { KnowledgeBaseDto } from "../dtos/KnowledgeBaseDto";
import { Link, useLoaderData, useSearchParams } from "react-router";
import { useState, useEffect } from "react";
import { useDebounce } from "use-debounce";
import { KbSearchResultDto } from "../dtos/KbSearchResultDto";
import ColorBorderUtils from "~/utils/shared/colors/ColorBorderUtils";
import { Colors } from "~/application/enums/shared/Colors";
import { KbArticleDto } from "../dtos/KbArticleDto";
import ColorHoverUtils from "~/utils/shared/colors/ColorHoverUtils";
import { useTranslation } from "react-i18next";
export default function KbSearch({ kb, autoFocus }: { kb: KnowledgeBaseDto; autoFocus: boolean }) {
const { t } = useTranslation();
let data = useLoaderData<
| {
search: KbSearchResultDto | undefined;
}
| undefined
>();
let [search, setSearch] = useState(data?.search?.query);
let [debouncedQuery] = useDebounce(search, 500);
const [searchParams, setSearchParams] = useSearchParams();
useEffect(() => {
if (debouncedQuery) {
searchParams.set("q", debouncedQuery);
} else {
searchParams.delete("q");
}
if (searchParams.toString()) {
setSearchParams(searchParams);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedQuery]);
return (
<div className="w-full space-y-2">
<div className={clsx("relative", ColorTextUtils.getText400(kb.color), ColorFocusWithinUtils.getText600(kb.color))}>
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4">
<SearchIcon className="h-5 w-5" />
</div>
<input
autoFocus={autoFocus}
autoComplete="off"
className={clsx(
" focus:bg-background block w-full rounded-md border-0 px-6 py-5 pl-12 outline-hidden ring-1 ring-inset transition-all focus:ring-2 focus:ring-inset",
ColorBackgroundUtils.getBg800(kb.color),
ColorTextUtils.getText300(kb.color),
ColorPlaceholderUtils.getText400(kb.color),
ColorFocusUtils.getText600(kb.color),
ColorFocusUtils.getPlaceholder600(kb.color),
ColorFocusUtils.getRing600(kb.color),
ColorRingUtils.getRing800(kb.color)
)}
type="text"
aria-label={t("knowledgeBase.search.label")}
placeholder={t("knowledgeBase.search.placeholder")}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{search && (
<button type="button" className="absolute inset-y-0 right-0 flex items-center pr-4" onClick={() => setSearch("")} aria-label={t("knowledgeBase.search.clear")}>
<XIcon className="h-5 w-5" />
</button>
)}
</div>
{search && data?.search !== undefined && (
<div>
{!data.search || data.search.articles?.length === 0 ? (
<div className={clsx("rounded-md border", ColorBorderUtils.get300(kb.color), ColorBackgroundUtils.getBg50(kb.color))}>
<div className="rounded-md p-4">
<div className="flex">
<div className="shrink-0">
<InfoIcon className={clsx("h-5 w-5", ColorTextUtils.getText400(kb.color))} />
</div>
<div className="ml-3 w-full">
<h3 className={clsx("text-sm font-medium leading-5", ColorTextUtils.getText800(kb.color))}>
{t("knowledgeBase.search.noResults", { query: data.search.query })}
</h3>
<div className={clsx("mt-2 text-sm leading-5", ColorTextUtils.getText800(kb.color))}>
<div>
<p className="text-sm">{t("knowledgeBase.search.tryDifferent")}</p>
</div>
</div>
</div>
</div>
</div>
</div>
) : (
<div>
<div className="space-y-2">
{data.search.articles.map((item) => (
<Card key={item.id} kb={kb} item={item} highlightText={debouncedQuery ?? ""} />
))}
</div>
</div>
)}
</div>
)}
</div>
);
}
function Highlight({ className, color, children, highlightText }: { className: string; color: Colors; children: string; highlightText: string }) {
const parts = children.split(new RegExp(`(${highlightText})`, "gi"));
return (
<span className={clsx(className, " line-clamp-3")}>
{parts.map((part, i) =>
part.toLowerCase() === highlightText.toLowerCase() ? (
<span key={i} className={clsx(ColorBackgroundUtils.getBg400(color), ColorTextUtils.getText100(color))}>
{part}
</span>
) : (
part
)
)}
</span>
);
}
function Card({ kb, item, highlightText }: { kb: KnowledgeBaseDto; item: KbArticleDto; highlightText: string }) {
const contentLines = item.contentPublishedAsText.split("\n");
const highlightedLines = contentLines
.map((line, i) => ({ line, isHighlighted: line.toLowerCase().includes(highlightText.toLowerCase()), i }))
.filter(({ isHighlighted }) => isHighlighted)
.slice(0, 3);
return (
<div key={item.title} className={clsx("group rounded-md p-2 shadow-md", ColorBackgroundUtils.getBg800(kb.color), ColorHoverUtils.getBg900(kb.color))}>
<Link to={item.href} className="w-full">
<div className={clsx("space-y-1")}>
<h2>
<Highlight className={clsx(ColorTextUtils.getText100(kb.color))} color={kb.color} highlightText={highlightText}>
{item.title}
</Highlight>
</h2>
<p>
<Highlight className={clsx(ColorTextUtils.getText300(kb.color))} color={kb.color} highlightText={highlightText}>
{item.description}
</Highlight>
</p>
{highlightedLines.map(({ line, i }) => (
<p key={i}>
<Highlight className={clsx(ColorTextUtils.getText300(kb.color))} color={kb.color} highlightText={highlightText}>
{line}
</Highlight>
</p>
))}
</div>
</Link>
</div>
);
}