"use client";
import { CloseTiny } from "components/Icons/CloseTiny";
import { Input } from "components/Input";
import { useState, useRef } from "react";
import { useDebouncedEffect } from "src/hooks/useDebouncedEffect";
import { Popover } from "components/Popover";
import Link from "next/link";
import { searchTags, type TagSearchResult } from "actions/searchTags";
export const Tag = (props: {
name: string;
selected?: boolean;
onDelete?: (tag: string) => void;
className?: string;
}) => {
return (
{props.name}{" "}
{props.selected ? (
) : null}
);
};
export const TagSelector = (props: {
selectedTags: string[];
setSelectedTags: (tags: string[]) => void;
}) => {
return (
{props.selectedTags.length > 0 ? (
{props.selectedTags.map((tag) => (
{
props.setSelectedTags(
props.selectedTags.filter((t) => t !== tag),
);
}}
/>
))}
) : (
no tags selected
)}
);
};
export const TagSearchInput = (props: {
selectedTags: string[];
setSelectedTags: (tags: string[]) => void;
}) => {
let [tagInputValue, setTagInputValue] = useState("");
let [isOpen, setIsOpen] = useState(false);
let [highlightedIndex, setHighlightedIndex] = useState(0);
let [searchResults, setSearchResults] = useState([]);
let [isSearching, setIsSearching] = useState(false);
const placeholderInputRef = useRef(null);
let inputWidth = placeholderInputRef.current?.clientWidth;
// Fetch tags whenever the input value changes
useDebouncedEffect(
async () => {
setIsSearching(true);
const results = await searchTags(tagInputValue);
if (results) {
setSearchResults(results);
}
setIsSearching(false);
},
300,
[tagInputValue],
);
const filteredTags = searchResults
.filter((tag) => !props.selectedTags.includes(tag.name))
.filter((tag) =>
tag.name.toLowerCase().includes(tagInputValue.toLowerCase()),
);
const showResults = tagInputValue.length >= 3;
function clearTagInput() {
setHighlightedIndex(0);
setTagInputValue("");
}
function selectTag(tag: string) {
props.setSelectedTags([...props.selectedTags, tag]);
clearTagInput();
}
const handleKeyDown = (
e: React.KeyboardEvent,
) => {
if (!isOpen) return;
if (e.key === "ArrowDown") {
e.preventDefault();
setHighlightedIndex((prev) =>
prev < filteredTags.length ? prev + 1 : prev,
);
} else if (e.key === "ArrowUp") {
e.preventDefault();
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0));
} else if (e.key === "Enter") {
e.preventDefault();
selectTag(
userInputResult
? highlightedIndex === 0
? tagInputValue
: filteredTags[highlightedIndex - 1].name
: filteredTags[highlightedIndex].name,
);
clearTagInput();
} else if (e.key === "Escape") {
setIsOpen(false);
}
};
const userInputResult =
showResults &&
tagInputValue !== "" &&
!filteredTags.some((tag) => tag.name === tagInputValue);
return (
{
setTagInputValue(e.target.value);
setIsOpen(true);
setHighlightedIndex(0);
}}
onKeyDown={handleKeyDown}
onFocus={() => {
setIsOpen(true);
document.getElementById("tag-search-input")?.focus();
}}
/>
{
setIsOpen(!isOpen);
if (!isOpen)
setTimeout(() => {
document.getElementById("tag-search-input")?.focus();
}, 100);
}}
className="w-full p-2! min-w-xs text-primary"
sideOffset={-39}
onOpenAutoFocus={(e) => e.preventDefault()}
asChild
trigger={
}
noArrow
>
{
setTagInputValue(e.target.value);
setIsOpen(true);
setHighlightedIndex(0);
}}
onKeyDown={handleKeyDown}
onFocus={() => {
setIsOpen(true);
}}
/>
{props.selectedTags.length > 0 ? (
{props.selectedTags.map((tag) => (
{
props.setSelectedTags(
props.selectedTags.filter((t) => t !== tag),
);
}}
/>
))}
) : (
no tags selected
)}
{showResults ? (
<>
{userInputResult && (
{
selectTag(tagInputValue);
}}
/>
)}
{filteredTags.map((tag, i) => (
{
selectTag(tag.name);
}}
/>
))}
>
) : (
type at least 3 characters to search
)}
);
};
const TagResult = (props: {
name: string;
tagged: number;
onSelect: () => void;
index: number;
highlighted: boolean;
setHighlightedIndex: (i: number) => void;
}) => {
return (
);
};