163 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
}
|