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 Input from "~/components/custom_ui/input";
|
|
|
|
|
import { StaggeredFade } from "~/components/custom_ui/staggered-fade";
|
|
|
|
|
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,
|
2025-08-29 09:58:44 +02:00
|
|
|
containerClassName,
|
2025-10-20 11:56:54 +02:00
|
|
|
inputId,
|
2025-08-04 17:45:44 +02:00
|
|
|
}: {
|
2025-08-28 18:27:07 +02:00
|
|
|
className?: string;
|
|
|
|
|
optionBoxClassName?: string;
|
|
|
|
|
optionsClassName?: string;
|
2025-08-29 09:58:44 +02:00
|
|
|
containerClassName?: string;
|
2025-10-20 11:56:54 +02:00
|
|
|
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}
|
2025-10-20 11:56:54 +02:00
|
|
|
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;
|
2025-08-29 09:58:44 +02:00
|
|
|
containerClassName?: string;
|
2025-10-20 11:56:54 +02:00
|
|
|
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,
|
2025-08-29 09:58:44 +02:00
|
|
|
containerClassName,
|
2025-10-20 11:56:54 +02:00
|
|
|
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 (
|
2025-08-29 16:18:32 +02:00
|
|
|
<div className="z-[5000] w-full" ref={wrapperRef}>
|
2025-08-29 09:58:44 +02:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
2025-11-14 17:21:21 +01:00
|
|
|
"relative flex w-full items-center justify-center rounded-md bg-[color-mix(in_oklch,var(--fxd-secondary)50%,white)]",
|
|
|
|
|
|
2025-08-29 09:58:44 +02:00
|
|
|
containerClassName,
|
|
|
|
|
)}
|
2025-08-29 15:03:57 +02:00
|
|
|
id="searchContainer"
|
2025-08-29 09:58:44 +02:00
|
|
|
>
|
2025-10-20 11:56:54 +02:00
|
|
|
<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-14 17:21:21 +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-fxd-secondary",
|
2025-08-29 16:18:32 +02:00
|
|
|
className,
|
|
|
|
|
)}
|
2025-10-20 11:56:54 +02:00
|
|
|
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-14 17:21:21 +01:00
|
|
|
"absolute z-50 mt-1 max-h-64 w-full overflow-hidden rounded-md border border-fxd-foreground bg-fxd-secondary shadow-xs ring-1 ring-fxd-secondary",
|
|
|
|
|
|
2025-08-28 18:27:07 +02:00
|
|
|
optionBoxClassName,
|
|
|
|
|
)}
|
2025-08-29 15:03:57 +02:00
|
|
|
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 &&
|
2025-11-14 17:21:21 +01:00
|
|
|
"[&>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(
|
2025-11-14 17:21:21 +01:00
|
|
|
"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
|
|
|
};
|
|
|
|
|
|
2025-08-29 14:59:38 +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>(
|
2025-08-29 14:59:38 +02:00
|
|
|
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,
|
|
|
|
|
);
|
2025-08-29 14:59:38 +02:00
|
|
|
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
|
2025-11-14 17:21:21 +01:00
|
|
|
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
|
|
|
};
|