infoalloggi-monorepo/apps/infoalloggi/src/components/codiceRicerca.tsx

207 lines
5.7 KiB
TypeScript
Raw Normal View History

2025-08-04 17:45:44 +02:00
"use client";
2025-08-28 18:27:07 +02:00
import { CircleOff } from "lucide-react";
2025-08-04 17:45:44 +02:00
import Link from "next/link";
2025-08-28 18:27:07 +02:00
import { useRouter } from "next/router";
2025-08-04 17:45:44 +02:00
import { useEffect, useRef, useState } from "react";
import { StaggeredFade } from "~/components/custom_ui/staggered-fade";
import Input from "~/components/ui/input";
2025-08-04 17:45:44 +02:00
import { useClickOutside } from "~/hooks/useClickOutside";
2025-08-28 18:27:07 +02:00
import { cn } from "~/lib/utils";
import { useTranslation } from "~/providers/I18nProvider";
import { api } from "~/utils/api";
2025-08-04 17:45:44 +02:00
export const CodiceBox = ({
2025-08-28 18:27:07 +02:00
className,
optionBoxClassName,
optionsClassName,
containerClassName,
inputId,
2025-08-04 17:45:44 +02:00
}: {
2025-08-28 18:27:07 +02:00
className?: string;
optionBoxClassName?: string;
optionsClassName?: string;
containerClassName?: string;
inputId: string;
2025-08-04 17:45:44 +02:00
}) => {
2025-08-28 18:27:07 +02:00
const { data: codici, isLoading: loading } = api.annunci.getCodici.useQuery();
const { locale } = useTranslation();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<AutocompleteSearchBox
2025-08-29 16:18:32 +02:00
className={className}
containerClassName={containerClassName}
inputId={inputId}
2025-08-28 18:27:07 +02:00
key={locale}
loading={loading}
optionBoxClassName={optionBoxClassName}
2025-08-29 16:18:32 +02:00
options={codici || []}
2025-08-28 18:27:07 +02:00
optionsClassName={optionsClassName}
/>
);
2025-08-04 17:45:44 +02:00
};
type SearchBoxProps = {
2025-08-28 18:27:07 +02:00
options: string[];
loading: boolean;
className?: string;
optionBoxClassName?: string;
optionsClassName?: string;
containerClassName?: string;
inputId: string;
2025-08-04 17:45:44 +02:00
};
const AutocompleteSearchBox = ({
2025-08-28 18:27:07 +02:00
options,
loading,
className,
optionBoxClassName,
optionsClassName,
containerClassName,
inputId,
2025-08-04 17:45:44 +02:00
}: SearchBoxProps) => {
2025-08-28 18:27:07 +02:00
const router = useRouter();
const [open, setOpen] = useState<boolean>(false);
const [results, setResults] = useState<string[]>(options);
const [isFocused, setIsFocused] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState<string>("");
const [navigationEnter, setNavigationEnter] = useState<boolean>(false);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const wrapperRef = useRef<HTMLDivElement | null>(null);
const listRef = useRef<HTMLDivElement | null>(null);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
useClickOutside([wrapperRef, listRef], () => setOpen(false));
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
useEffect(() => {
if (searchValue.trim() === "" || searchValue.length === 0) {
setResults(options);
} else {
setResults(
options.filter((option) => option.includes(searchValue.trim())),
);
}
}, [searchValue, options]);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<div className="z-5000 w-full" ref={wrapperRef}>
<div
className={cn(
"relative flex w-full items-center justify-center rounded-md bg-secondary dark:bg-primary",
containerClassName,
)}
id="searchContainer"
>
<label className="sr-only" htmlFor={inputId || "search"}>
2025-08-28 18:27:07 +02:00
Search Box
</label>
<Input
2025-08-29 16:18:32 +02:00
className={cn(
2025-11-25 09:57:31 +01:00
"z-10 cursor-pointer search-cancel:cursor-pointer rounded-md border-fxd-foreground bg-transparent text-fxd-foreground text-xl search-cancel:brightness-75 search-cancel:grayscale placeholder:font-bold placeholder:text-primary focus:ring-0 dark:bg-transparent dark:hover:bg-input/10",
2025-08-29 16:18:32 +02:00
className,
)}
id={inputId || "search"}
2025-08-29 16:18:32 +02:00
onBlur={() => {
setIsFocused(false);
}}
2025-08-28 18:27:07 +02:00
onChange={(e) => setSearchValue(e.target.value)}
onFocus={() => {
setOpen(true);
setIsFocused(true);
}}
onKeyDown={async (e) => {
if (e.key === "Enter" && results[0] && results.length > 0) {
setNavigationEnter(true);
await router.push(`/annuncio/${results[0].trim()}`);
}
}}
2025-08-29 16:18:32 +02:00
type="search"
value={searchValue}
2025-08-28 18:27:07 +02:00
/>
<MovingText hidden={isFocused || searchValue !== ""} />
</div>
{open && (
<div
className={cn(
2025-11-25 09:57:31 +01:00
"absolute z-50 mt-1 max-h-64 w-full overflow-hidden rounded-md border border-fxd-foreground bg-fxd-secondary",
2025-08-28 18:27:07 +02:00
optionBoxClassName,
)}
id="searchOptionsBox"
2025-08-29 16:18:32 +02:00
ref={listRef}
2025-08-28 18:27:07 +02:00
>
<ul
className={cn(
"max-h-64 w-full overflow-y-auto",
navigationEnter &&
"[&>li:first-child>a]:underline [&>li:first-child>a]:underline-offset-2",
"scrollbar-default",
2025-08-28 18:27:07 +02:00
)}
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: <need list>
role="listbox"
>
{loading ? (
2025-10-10 16:18:43 +02:00
<li className={"px-3 py-2 text-center text-neutral-600 text-xl"}>
2025-08-28 18:27:07 +02:00
<span>Caricamento...</span>
</li>
) : (
<>
{results.length === 0 && (
<li
2025-10-10 16:18:43 +02:00
className={"px-3 py-2 text-center text-neutral-600 text-xl"}
2025-08-28 18:27:07 +02:00
>
<span>
<CircleOff className="inline size-6" />
</span>
</li>
)}
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
{results.map((item) => (
<li key={item}>
<Link
className={cn(
"flex w-full cursor-pointer items-center justify-between border-fxd-foreground border-b bg-[color-mix(in_oklch,var(--fxd-secondary)50%,white)] px-4 py-2 text-fxd-foreground text-xl duration-150 hover:bg-fxd-secondary hover:text-[color-mix(in_oklch,var(--fxd-foreground)90%,black)] hover:underline hover:underline-offset-2",
2025-08-28 18:27:07 +02:00
optionsClassName,
)}
2025-08-29 16:18:32 +02:00
href={`/annuncio/${item}`}
2025-08-28 18:27:07 +02:00
>
{item}
</Link>
</li>
))}
</>
)}
</ul>
</div>
)}
</div>
);
2025-08-04 17:45:44 +02:00
};
const backupFrase = "Inserisci il codice 👈";
2025-08-04 17:45:44 +02:00
const MovingText = ({ hidden }: { hidden: boolean }) => {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation();
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
const [randomFrase, setRandomFrase] = useState<string>(
t.code_search_frases[0] || backupFrase,
2025-08-28 18:27:07 +02:00
);
useEffect(() => {
const interval = setInterval(() => {
const randomIndex = Math.floor(
Math.random() * t.code_search_frases.length,
);
setRandomFrase(t.code_search_frases[randomIndex] || backupFrase);
2025-08-28 18:27:07 +02:00
}, 5000);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return () => clearInterval(interval);
}, [t]);
2025-08-04 17:45:44 +02:00
2025-08-28 18:27:07 +02:00
return (
<StaggeredFade
className="pointer-events-none absolute select-none font-bold text-2xl text-primary dark:text-primary-foreground"
2025-08-28 18:27:07 +02:00
hide={hidden}
2025-08-29 16:18:32 +02:00
key={randomFrase}
text={randomFrase}
2025-08-28 18:27:07 +02:00
/>
);
2025-08-04 17:45:44 +02:00
};