refactor: remove unused annunci data fetching and components; update ricerca logic

- Deleted the `annunci.ts` file which contained the database fetching logic.
- Removed the `TestAppPage` component that relied on the deleted data fetching.
- Eliminated the `AnnunciList` component that was dependent on the removed data.
- Updated `annunci_map.tsx` to handle optional `selectComune` parameter.
- Refactored `annunci_tutorial.tsx` to use `buildRicercaUrl` for navigation.
- Adjusted `annuncio_card.tsx` for consistent styling and layout changes.
- Modified `codiceRicerca.tsx` to fix z-index issue.
- Enhanced `failed-loading.tsx` and `500Auth.tsx` for responsive design.
- Updated `annunci.tsx` to include loading state for map and annunci list.
- Refined filter handling in `Filters` component to use a single `setAllParams` method.
- Introduced `FrequentSearches` component for better user navigation.
- Updated `RicercaProvider` to streamline state management and URL building.
- Adjusted server-side logic in `annunci.controller.ts` to ensure valid data.
This commit is contained in:
Marco Pedone 2026-01-20 18:38:17 +01:00
parent cb97919157
commit edaee780d6
22 changed files with 335 additions and 439 deletions

View file

@ -59,27 +59,27 @@ const OnboardingServizio = ({
<Text className="font-semibold">
Annunci selezionati per te:
</Text>
<Section className="mb-[30px]" key={annuncio.codice}>
<Row className="mb-[12px]">
<Section className="mb-7.5" key={annuncio.codice}>
<Row className="mb-3">
<Img
alt={`Annuncio image - ${annuncio.codice}`}
className="block w-[240px] rounded-[4px] object-cover object-center"
className="block w-60 rounded-lg object-cover object-center"
height="140px"
src={`${env.BASE_URL}/storage-api/get/${annuncio.immagine}?media=image`}
width="100%"
/>
</Row>
<Row className="block w-full">
<div className="mb-[5px] flex h-[24px] w-fit items-center justify-center gap-1 rounded-full bg-indigo-600 px-2 font-semibold text-[12px] text-white leading-0">
<div className="mb-1.25 flex h-6 w-fit items-center justify-center gap-1 rounded-full bg-indigo-600 px-2 font-semibold text-[12px] text-white leading-0">
<strong>Codice: </strong> {annuncio.codice}
</div>
<Heading
as="h2"
className="mt-0 mb-[6px] font-bold text-[16px] leading-none"
className="mt-0 mb-1.5 font-bold text-[16px] leading-none"
>
{annuncio.titolo || "N/A"}
</Heading>
<Text className="m-0 text-[14px] text-gray-500 leading-[24px]">
<Text className="m-0 text-[14px] text-gray-500 leading-6">
<strong>Prezzo:</strong>
{(annuncio.prezzo / 1e2).toFixed(2)}
</Text>

View file

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://www.infoalloggi.it</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>1.00</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/annunci</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/prezzi</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/guida</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/contatti</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/proprietari</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/annunci?comune=Bassano+del+Grappa</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/annunci?tipo=Transitorio</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/annunci?comune=Bassano+del+Grappa&amp;tipo=Transitorio</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/annunci?tipo=Stabile</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/annunci?comune=Marostica</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/chi-siamo</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/termini-condizioni</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://www.infoalloggi.it/privacy-policy</loc>
<lastmod>2025-08-23T15:09:55+00:00</lastmod>
<priority>0.80</priority>
</url>
</urlset>

View file

@ -1,44 +0,0 @@
import type { Metadata } from "next";
import "~/styles/globals.css";
import "react-day-picker/dist/style.css";
import { inter } from "~/utils/fonts";
export const metadata: Metadata = {
description: "Trova casa ora con Infoalloggi",
icons: {
apple: {
sizes: "180x180",
type: "image/png",
url: "/favicon/apple-touch-icon.png",
},
icon: [
{ sizes: "32x32", type: "image/png", url: "/favicon/favicon-32x32.png" },
{ sizes: "16x16", type: "image/png", url: "/favicon/favicon-16x16.png" },
],
other: [
{
color: "#5bbad5",
rel: "mask-icon",
url: "/favicon/safari-pinned-tab.svg",
},
],
},
manifest: "/site.webmanifest",
title: "Infoalloggi.it",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html
className={`${inter.variable} scrollbar-default font-sans`}
lang="en"
suppressHydrationWarning
>
<body>{children}</body>
</html>
);
}

View file

@ -1,20 +0,0 @@
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import { db } from "~/server/db";
export async function getDataFromDB() {
try {
// Example query - adjust to your database schema
const data = await db
.selectFrom("annunci")
.select("codice")
.where("web", "=", true)
.where("tipo", "=", TipologiaPosizioneEnum.Transitorio)
.where("stato", "=", "Attivo")
.execute();
return data;
} catch (error) {
console.error("Database error:", error);
return [];
}
}

View file

@ -1,22 +0,0 @@
import { Suspense } from "react";
import { getDataFromDB } from "~/_app/lib/annunci";
import AnnunciList from "~/_app/ui/annunci";
import { LoadingPage } from "~/components/loading";
export default function TestAppPage() {
const annunci = getDataFromDB();
return (
<div className="p-8">
<h1 className="mb-4 font-bold text-2xl">Test App Page</h1>
<div className="rounded-lg border bg-gray-50 p-4">
<h2 className="mb-2 font-semibold text-lg">Data from Database:</h2>
<Suspense fallback={<LoadingPage />}>
<AnnunciList annunci={annunci} />
</Suspense>
</div>
</div>
);
}

View file

@ -1,19 +0,0 @@
"use client";
import { use } from "react";
import type { Annunci } from "~/schemas/public/Annunci";
type AnnunciProps = {
annunci: Promise<Pick<Annunci, "codice">[]>;
};
export default function AnnunciList({ annunci }: AnnunciProps) {
const allAnnunci = use(annunci);
return (
<ul>
{allAnnunci.map((ann) => (
<li key={ann.codice}>{ann.codice}</li>
))}
</ul>
);
}

View file

@ -53,14 +53,14 @@ const MapDisplay = memo(
selectMaxBudget,
}: {
selectTipo: string;
selectComune: string;
selectComune: string | null;
selectConsegna: number[] | null;
setSelected: Dispatch<SetStateAction<AnnuncioRicercaWPosition | null>>;
selectMinBudget: number | null;
selectMaxBudget: number | null;
}) => {
const { data, isLoading } = api.annunci.getMapping.useQuery({
comune: selectComune,
comune: selectComune || undefined,
consegna: selectConsegna || undefined,
tipologia: selectTipo,
minBudget: selectMinBudget || undefined,

View file

@ -14,6 +14,7 @@ import {
DialogTitle,
} from "~/components/ui/dialog";
import { useTranslation } from "~/providers/I18nProvider";
import { buildRicercaUrl } from "~/providers/RicercaProvider";
const COOKIE_KEY = "tipologia_tutorial_shown";
@ -27,7 +28,7 @@ export const CTA_TipologiaModal = () => {
expires: add(new Date(), { days: 30 }), // Set cookie to expire in 30 days
});
if (v !== null) {
window.location.assign(`/annunci?tipo=${v}`);
window.location.assign(buildRicercaUrl({ tipo: v }));
}
setOpen(false);
};

View file

@ -99,7 +99,7 @@ const SkeletonCardAnnuncio = () => {
<Skeleton className="h-10 w-full rounded-md" />
<Skeleton className="h-10 w-full rounded-md" />
</div>
<div className="flex h-[8.5rem] flex-col justify-between">
<div className="flex h-34 flex-col justify-between">
<Skeleton className="h-20 w-full rounded-md" />
<div className="flex h-9 items-center justify-around gap-1.5">
@ -451,7 +451,7 @@ export const CarouselAnnuncio = ({
<Image
alt={img.img}
className={cn(
"aspect-square max-h-92 w-full rounded-md bg-[#e6e9ec] object-cover sm:max-h-80 xl:max-h-[25rem]",
"aspect-square max-h-92 w-full rounded-md bg-[#e6e9ec] object-cover sm:max-h-80 xl:max-h-100",
immagini.length === 1 && "object-contain",
)}
height={400}
@ -484,7 +484,7 @@ export const CarouselAnnuncio = ({
key={`videoplayer-${video.video}`}
>
<VideoPlayer
className="aspect-square max-h-92 object-cover sm:max-h-80 xl:max-h-[25rem]"
className="aspect-square max-h-92 object-cover sm:max-h-80 xl:max-h-100"
coverImage={getStorageUrl({
storageId: video.thumb,
params: {

View file

@ -82,7 +82,7 @@ const AutocompleteSearchBox = ({
}, [searchValue, options]);
return (
<div className="z-[5000] w-full" ref={wrapperRef}>
<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",

View file

@ -9,7 +9,7 @@ export default function FailedAnnuncioLoading() {
const { t } = useTranslation();
return (
<main>
<div className="mx-auto flex h-screen max-w-screen-xl items-center justify-start px-4 md:px-8">
<div className="mx-auto flex h-screen max-w-7xl items-center justify-start px-4 md:px-8">
<div className="mx-auto max-w-lg space-y-3 text-center">
<HomeIcon className="mx-auto size-20 text-destructive" />
<h3 className="font-semibold text-4xl text-accent-foreground sm:text-5xl">

View file

@ -0,0 +1,97 @@
import { ArrowRight } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "~/providers/I18nProvider";
import { buildRicercaUrl } from "~/providers/RicercaProvider";
import { Button } from "./ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card";
const it = {
title: "Ricerche frequenti",
transitorio: "Transitori",
stabili: "Stabili",
};
const en = {
title: "Frequent Searches",
transitorio: "Short-term",
stabili: "Stable",
};
export const FrequentSearches = () => {
const { locale } = useTranslation();
const t = locale === "en" ? en : it;
return (
<Card className="mx-auto mb-4 max-w-4xl gap-4 border-0 bg-secondary shadow-none">
<CardHeader>
<CardTitle className="text-center font-bold text-3xl sm:text-2xl">
{t.title}
</CardTitle>
</CardHeader>
<CardContent className="mx-auto flex max-w-fit flex-wrap items-center justify-center gap-4 px-2 sm:px-6">
<Link href={buildRicercaUrl({ tipo: "Transitorio" })}>
<Button
className="flex items-center gap-1 border-2 border-transitorio bg-transitorio text-lg text-white"
size="lg"
variant="flat"
>
{t.transitorio}
<ArrowRight />
</Button>
</Link>
<Link href={buildRicercaUrl({ tipo: "Stabile" })}>
<Button
className="flex items-center gap-1 border-2 border-stabile bg-stabile text-lg text-white"
size="lg"
variant="flat"
>
{t.stabili}
<ArrowRight />
</Button>
</Link>
<Link
href={buildRicercaUrl({
comune: "Bassano del Grappa",
})}
>
<Button
className="flex items-center gap-1 border-2 border-primary text-lg"
size="lg"
>
Bassano del Grappa
<ArrowRight />
</Button>
</Link>
<Link
href={buildRicercaUrl({
comune: "Bassano del Grappa",
tipo: "Transitorio",
})}
>
<Button
className="flex items-center gap-1 border-2 border-transitorio bg-transitorio text-lg text-white"
size="lg"
variant="flat"
>
{t.transitorio} Bassano
<ArrowRight />
</Button>
</Link>
<Link
href={buildRicercaUrl({
comune: "Marostica",
})}
>
<Button
className="flex items-center gap-1 border-2 border-primary text-lg"
size="lg"
>
Marostica
<ArrowRight />
</Button>
</Link>
</CardContent>
</Card>
);
};

View file

@ -35,7 +35,7 @@ export const AnnunciCompatibili = () => {
Esplora Annunci Compatibili
</Button>
</DialogTrigger>
<DialogContent className="max-w-xl p-2 sm:max-w-5xl sm:p-6 md:max-w-7xl 2xl:max-w-[100rem]">
<DialogContent className="max-w-xl p-2 sm:max-w-5xl sm:p-6 md:max-w-7xl 2xl:max-w-400">
<DialogHeader className="flex-row flex-wrap items-center gap-5 pt-6">
<DialogTitle>
Annunci compatibili con i parametri del servizio

View file

@ -104,7 +104,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => {
stato: z.string().nullable(),
// DATI CONTRATTO
tipo: z.string().nullable(),
tipo: z.string(),
titolo_en: z.string().nullable(),
titolo_it: z.string().nullable(),
unita_condominio: z.number().nullable(),

View file

@ -235,6 +235,7 @@ export type LangDict = {
endtxt: string;
endtxt2: string;
};
annunci: {
titolo: string;
fs_tipologia: {

View file

@ -5,7 +5,7 @@ export default function Custom500Auth() {
const { t } = useTranslation();
return (
<main>
<div className="mx-auto flex h-screen max-w-screen-xl items-center justify-start px-4 md:px-8">
<div className="mx-auto flex h-screen max-w-7xl items-center justify-start px-4 md:px-8">
<div className="mx-auto max-w-lg space-y-3 text-center">
<ShieldExclamationIcon className="mx-auto size-20 text-red-600" />
<h3 className="font-semibold text-4xl text-accent-foreground sm:text-5xl">

View file

@ -105,14 +105,17 @@ export const getServerSideProps = (async () => {
}) satisfies GetServerSideProps<AnnunciPageProps>;
const Ricerca = () => {
const { map } = useRicerca();
const { map, isLoading } = useRicerca();
return (
<div className="mx-auto w-full space-y-5">
<RicercaHeader />
<div className="relative flex w-full gap-4">
<RicercaSideFilters />
<div className="flex-1">{map ? <MapSection /> : <AnnunciList />}</div>
<div className="flex-1">
{isLoading ? <LoadingPage /> : map ? <MapSection /> : <AnnunciList />}
</div>
</div>
</div>
);
@ -122,14 +125,12 @@ const Filters = () => {
const { t } = useTranslation();
const {
tipo,
setTipo,
comune,
setComune,
sort,
setSort,
comuniOptions,
tipologieOptions,
ResetParams,
setAllParams,
reset,
nFiltri,
} = useRicerca();
@ -138,39 +139,55 @@ const Filters = () => {
<SelectFilter
defaultValue={""}
handleReset={async () => {
await setTipo("");
await setAllParams({
tipo: "",
});
}}
id="tipologia-filter"
label={t.annunci.fs_tipologia.titolo}
onValueChange={async (value: string) => {
await setTipo(value);
await setAllParams({
tipo: value,
});
}}
options={tipologieOptions}
placeholder={t.seleziona_placeholder}
value={tipo}
/>
<SelectFilter
defaultValue={""}
defaultValue={undefined}
handleReset={async () => {
await setComune(null);
await setAllParams({
comune: null,
});
}}
id="comune-filter"
key={comune}
label={t.annunci.comune}
onValueChange={async (value: string) => {
await setComune(value);
await setAllParams({
comune: value,
});
}}
options={comuniOptions}
placeholder={t.seleziona_placeholder}
value={comune}
value={comune || undefined}
/>
<ConsegnaFilter />
<BudgetFilters />
<SelectFilter
defaultValue={sort}
handleReset={async () => {
await setSort("Recenti");
await setAllParams({
sort: "Recenti",
});
}}
id="sort-filter"
label={t.ordina_per}
onValueChange={async (value: SortTypes) => {
await setSort(value);
await setAllParams({
sort: value,
});
}}
options={["Recenti", "Prezzo", "Consegna", "Mq"]}
placeholder={t.seleziona_placeholder}
@ -179,7 +196,7 @@ const Filters = () => {
<Button
className="flex flex-row gap-2"
disabled={nFiltri === 0}
onClick={ResetParams}
onClick={reset}
variant="destructive"
>
<FilterX /> {t.annunci.filtri_reset}
@ -189,8 +206,11 @@ const Filters = () => {
};
const RicercaSideFilters = () => {
const isBigScreen = useMediaQuery("(min-width: 96rem)");
if (!isBigScreen) return null;
return (
<div className="hidden h-fit w-46 max-w-46 shrink-0 grow-0 flex-col gap-3 overflow-clip rounded-md bg-secondary p-2 outline-neutral-300 2xl:flex">
<div className="flex h-fit w-46 max-w-46 shrink-0 grow-0 flex-col gap-3 overflow-clip rounded-md bg-secondary p-2 outline-neutral-300">
<Filters />
</div>
);
@ -230,7 +250,7 @@ const DrowdownFilters = () => {
};
const RicercaHeader = () => {
const { map, setMap } = useRicerca();
const { map, setAllParams } = useRicerca();
return (
<div className="mx-auto w-full max-w-10xl space-y-5">
@ -240,7 +260,9 @@ const RicercaHeader = () => {
<Button
className="flex w-fit flex-row items-center gap-2 md:w-24"
onClick={async () => {
await setMap((v) => !v);
await setAllParams({
map: !map,
});
}}
variant="default"
>
@ -281,10 +303,10 @@ const AnnunciList = () => {
map,
minBudget,
maxBudget,
isLoading: ricercaLoading,
isLoading: isUpdatingRicerca,
} = useRicerca();
const { data, status } = api.annunci.annunciRicerca.useQuery(
const { data, status, isLoading } = api.annunci.annunciRicerca.useQuery(
{
comune: comune || undefined,
consegna: consegna || undefined,
@ -294,13 +316,13 @@ const AnnunciList = () => {
maxBudget: maxBudget || undefined,
},
{
enabled: !map,
enabled: !map && !isUpdatingRicerca,
placeholderData: keepPreviousData,
staleTime: 5000,
},
);
if (ricercaLoading) return <LoadingPage />;
if (isLoading) return <LoadingPage />;
if (status === "error") return <Status500 />;
return (
@ -325,6 +347,7 @@ const AnnunciList = () => {
interface SelectFilterProps<T> {
label: string;
id: string;
options: string[];
defaultValue?: T;
value: T | undefined;
@ -335,6 +358,7 @@ interface SelectFilterProps<T> {
export const SelectFilter = <T extends string>({
label,
id,
options,
defaultValue,
value,
@ -350,6 +374,7 @@ export const SelectFilter = <T extends string>({
"font-semibold text-muted-foreground",
value !== defaultValue && "text-primary",
)}
htmlFor={id}
>
{label}
</Label>
@ -369,7 +394,10 @@ export const SelectFilter = <T extends string>({
onValueChange={onValueChange}
value={value}
>
<SelectTrigger className="w-full bg-primary-foreground text-primary">
<SelectTrigger
className="w-full bg-primary-foreground text-primary"
id={id}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
@ -386,12 +414,7 @@ export const SelectFilter = <T extends string>({
const ConsegnaFilter = () => {
const { t } = useTranslation();
const {
consegna: consegna2,
setConsegna: setConsegna2,
consegnaOptions,
} = useRicerca();
const { consegna: consegna2, setAllParams, consegnaOptions } = useRicerca();
const mappedConsegnaOptions = useMemo(() => {
if (consegnaOptions.length === 0) return [];
@ -404,14 +427,17 @@ const ConsegnaFilter = () => {
const onValueChange = async (value: number) => {
if (consegna2?.includes(value)) {
// Remove value
const newConsegna2 = consegna2.filter((c) => c !== value);
await setConsegna2(newConsegna2.length > 0 ? newConsegna2 : null);
await setAllParams({
consegna: newConsegna2.length > 0 ? newConsegna2 : null,
});
return;
}
// Add value
const newConsegna2 = consegna2 ? [...consegna2, value] : [value];
await setConsegna2(newConsegna2);
await setAllParams({
consegna: newConsegna2,
});
return;
};
return (
@ -429,7 +455,9 @@ const ConsegnaFilter = () => {
<button
className="cursor-pointer"
onClick={async () => {
await setConsegna2(null);
await setAllParams({
consegna: null,
});
}}
type="button"
>
@ -461,20 +489,24 @@ const ConsegnaFilter = () => {
const BudgetFilters = () => {
const { t } = useTranslation();
const { minBudget, setMinBudget, maxBudget, setMaxBudget } = useRicerca();
const { minBudget, maxBudget, setAllParams } = useRicerca();
const minDebounced = useCallback(
debounce(async (value: number | null) => {
await setMinBudget(value);
await setAllParams({
minBudget: value,
});
}, 500),
[setMinBudget],
[setAllParams],
);
const maxDebounced = useCallback(
debounce(async (value: number | null) => {
await setMaxBudget(value);
await setAllParams({
maxBudget: value,
});
}, 500),
[setMaxBudget],
[setAllParams],
);
return (
@ -486,6 +518,7 @@ const BudgetFilters = () => {
"font-semibold text-muted-foreground",
minBudget !== null && "text-primary",
)}
htmlFor="prezzoMIN"
>
{t.annunci.budgetMin}
</Label>
@ -493,7 +526,9 @@ const BudgetFilters = () => {
<button
className="cursor-pointer"
onClick={async () => {
await setMinBudget(null);
await setAllParams({
minBudget: null,
});
}}
type="button"
>
@ -507,7 +542,7 @@ const BudgetFilters = () => {
decimalScale={2}
decimalSeparator=","
fixedDecimalScale
id="prezzo"
id="prezzoMIN"
min={0}
onValueChange={(v) => {
if (v && v > 0) {
@ -529,6 +564,7 @@ const BudgetFilters = () => {
"font-semibold text-muted-foreground",
maxBudget !== null && "text-primary",
)}
htmlFor="prezzoMAX"
>
{t.annunci.budgetMax}
</Label>
@ -536,7 +572,9 @@ const BudgetFilters = () => {
<button
className="cursor-pointer"
onClick={async () => {
await setMaxBudget(null);
await setAllParams({
maxBudget: null,
});
}}
type="button"
>
@ -549,7 +587,7 @@ const BudgetFilters = () => {
decimalScale={2}
decimalSeparator=","
fixedDecimalScale
id="prezzo"
id="prezzoMAX"
min={0}
onValueChange={(v) => {
if (v && v > 0) {

View file

@ -66,6 +66,7 @@ import { getStorageUrl } from "~/lib/storage_utils";
import { cn } from "~/lib/utils";
import { AnnuncioContext } from "~/providers/AnnuncioProvider";
import { useTranslation } from "~/providers/I18nProvider";
import { buildRicercaUrl } from "~/providers/RicercaProvider";
import { useSession } from "~/providers/SessionProvider";
import type { Annunci } from "~/schemas/public/Annunci";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
@ -659,7 +660,7 @@ const AnnuncioFooter = ({
external_videos,
updated_at,
}: {
tipo: string | null;
tipo: string;
comune: string | null;
consegna: number | null;
videos: Pick<VideosRefs, "video" | "thumb">[];
@ -706,14 +707,14 @@ const AnnuncioFooter = ({
</div>
<Link
href={{
pathname: "/annunci",
query: {
c: comune,
f: consegna,
t: tipo,
},
}}
href={buildRicercaUrl({
comune: comune || undefined,
consegna: consegna ? [consegna] : undefined,
tipo:
tipo in ["Transitorio", "Stabile"]
? (tipo as "Transitorio" | "Stabile")
: undefined,
})}
target="_blank"
>
<Button className="my-10 w-full text-xl" variant="secondary">

View file

@ -1,14 +1,13 @@
import { ArrowRight, Telescope } from "lucide-react";
import { Telescope } from "lucide-react";
import type { NextPage } from "next";
import Head from "next/head";
import Link from "next/link";
import { AccordionComp } from "~/components/accordionComp";
import { CodiceBox } from "~/components/codiceRicerca";
import { ComeFunziona } from "~/components/expand_guida";
import { FrequentSearches } from "~/components/frequent_searches";
import { PricingChoice } from "~/components/prezzi";
import { HeroSvg, LogoSvg } from "~/components/svgs";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { env } from "~/env";
import type { LangDict } from "~/i18n/locales";
import { useTranslation } from "~/providers/I18nProvider";
@ -37,11 +36,11 @@ const Home: NextPage = () => {
/>
<meta content="Infoalloggi.it" property="og:site_name" />
</Head>
<div className="mx-5 mt-0 mb-12 sm:mt-[2rem]">
<div className="mx-5 mt-0 mb-12 sm:mt-8">
<main className="mx-auto mb-4 max-w-8xl space-y-5 lg:mb-20">
<div className="flex flex-col items-center justify-center lg:flex-row lg:justify-around">
<div className="-mt-2 xs:mt-5 flex h-[19rem] max-w-full items-center justify-center min-[22rem]:h-[23rem] min-[25rem]:h-[26rem]">
<div className="flex scale-50 items-center justify-center drop-shadow-xl sm:scale-100 md:max-h-96 min-[20rem]:scale-[0.6] min-[22rem]:scale-[0.7] min-[25rem]:scale-[0.8]">
<div className="-mt-2 xs:mt-5 flex h-76 max-w-full items-center justify-center min-[22rem]:h-92 min-[25rem]:h-104">
<div className="flex scale-50 xxs:scale-[0.6] items-center justify-center drop-shadow-xl sm:scale-100 md:max-h-96 min-[22rem]:scale-[0.7] min-[25rem]:scale-[0.8]">
<HeroSvg />
</div>
</div>
@ -77,7 +76,7 @@ const TrovaCasaCTA = (props: { testi: LangDict }) => {
<span className="text-destructive"> {testi.index.titolo_2}</span>{" "}
{testi.index.titolo_3}
</h1>
<LogoSvg className="h-[3rem] w-auto sm:h-[4rem] md:h-[5rem]" />
<LogoSvg className="h-12 w-auto sm:h-16 md:h-20" />
<br />
<div className="flex flex-col space-y-4 lg:justify-start">
@ -106,68 +105,3 @@ const TrovaCasaCTA = (props: { testi: LangDict }) => {
</div>
);
};
const FrequentSearches = () => {
const { locale } = useTranslation();
return (
<Card className="mx-auto mb-4 max-w-4xl gap-4 border-0 bg-secondary shadow-none">
<CardHeader>
<CardTitle className="text-center font-bold text-3xl sm:text-2xl">
{locale === "en" ? "Frequent Searches" : "Ricerche frequenti"}
</CardTitle>
</CardHeader>
<CardContent className="mx-auto flex max-w-fit flex-wrap items-center justify-center gap-4 px-2 sm:px-6">
<Link href="/annunci?tipo=Transitorio">
<Button
className="flex items-center gap-1 border-2 border-transitorio bg-transitorio text-black text-lg"
size="lg"
variant="flat"
>
Transitori
<ArrowRight />
</Button>
</Link>
<Link href="/annunci?tipo=Stabile">
<Button
className="flex items-center gap-1 border-2 border-stabile bg-stabile text-black text-lg"
size="lg"
variant="flat"
>
Stabili
<ArrowRight />
</Button>
</Link>
<Link href="/annunci?comune=Bassano+del+Grappa">
<Button
className="flex items-center gap-1 border-2 border-primary text-lg"
size="lg"
>
Bassano del Grappa
<ArrowRight />
</Button>
</Link>
<Link href="/annunci?comune=Bassano+del+Grappa&tipo=Transitorio">
<Button
className="flex items-center gap-1 border-2 border-transitorio bg-transitorio text-black text-lg"
size="lg"
variant="flat"
>
Transitori Bassano
<ArrowRight />
</Button>
</Link>
<Link href="/annunci?comune=Marostica">
<Button
className="flex items-center gap-1 border-2 border-primary text-lg"
size="lg"
>
Marostica
<ArrowRight />
</Button>
</Link>
</CardContent>
</Card>
);
};

View file

@ -1,5 +1,6 @@
import type { GetServerSideProps } from "next";
import { env } from "~/env";
import { buildRicercaUrl } from "~/providers/RicercaProvider";
import { generateSSGHelper } from "~/server/utils/ssgHelper";
async function generateSiteMap() {
@ -13,10 +14,11 @@ async function generateSiteMap() {
`${env.NEXT_PUBLIC_BASE_URL}/proprietari`,
`${env.NEXT_PUBLIC_BASE_URL}/guida`,
`${env.NEXT_PUBLIC_BASE_URL}/annunci`,
`${env.NEXT_PUBLIC_BASE_URL}/annunci?t=Transitorio`,
`${env.NEXT_PUBLIC_BASE_URL}/annunci?c=Bassano+del+Grappa`,
`${env.NEXT_PUBLIC_BASE_URL}/annunci?t=Stabile`,
`${env.NEXT_PUBLIC_BASE_URL}/annunci?c=Marostica`,
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ tipo: "Transitorio" })}`,
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ comune: "Bassano del Grappa" })}`,
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ comune: "Bassano del Grappa", tipo: "Transitorio" })}`,
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ tipo: "Stabile" })}`,
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ comune: "Marostica" })}`,
];
const allUrls = [...staticUrls, ...dynamicUrls];

View file

@ -5,64 +5,85 @@ import {
parseAsInteger,
parseAsString,
parseAsStringLiteral,
useQueryState,
useQueryStates,
} from "nuqs";
import {
createContext,
type FC,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
useTransition,
} from "react";
const sortOptions = ["Recenti", "Prezzo", "Consegna", "Mq"] as const;
export type SortTypes = (typeof sortOptions)[number];
type RicercaUrlParams = {
map: boolean;
tipo: string;
comune: string;
consegna: number[];
minBudget: number;
maxBudget: number;
sort: SortTypes;
};
export const paramsMapping = {
map: "m",
tipo: "t",
comune: "c",
consegna: "f",
minBudget: "bmin",
maxBudget: "bmax",
sort: "s",
} as const;
export const buildRicercaUrl = (params: Partial<RicercaUrlParams>): string => {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
const mappedKey = paramsMapping[key as keyof typeof paramsMapping];
if (value !== undefined && value !== null) {
searchParams.set(mappedKey, value.toString());
}
});
const queryString = searchParams.toString();
return queryString ? `/annunci?${queryString}` : "/annunci";
};
type RicercaContextType = {
map: boolean;
setMap: (
value: boolean | ((old: boolean) => boolean | null) | null,
) => Promise<URLSearchParams>;
tipo: string;
setTipo: (
value: string | ((old: string) => string | null) | null,
) => Promise<URLSearchParams>;
comune: string;
setComune: (
value: string | ((old: string) => string | null) | null,
) => Promise<URLSearchParams>;
consegna: number[] | null;
setConsegna: (
value: number[] | ((old: number[] | null) => number[] | null) | null,
) => Promise<URLSearchParams>;
minBudget: number | null;
setMinBudget: (
value: number | ((old: number | null) => number | null) | null,
) => Promise<URLSearchParams>;
maxBudget: number | null;
setMaxBudget: (
value: number | ((old: number | null) => number | null) | null,
) => Promise<URLSearchParams>;
tipo: RicercaUrlParams["tipo"];
comune: RicercaUrlParams["comune"] | null;
consegna: RicercaUrlParams["consegna"] | null;
minBudget: RicercaUrlParams["minBudget"] | null;
maxBudget: RicercaUrlParams["maxBudget"] | null;
sort: SortTypes;
setSort: (
value: SortTypes | ((old: SortTypes) => SortTypes | null) | null,
setAllParams: (
params: Partial<{
tipo: RicercaUrlParams["tipo"];
comune: RicercaUrlParams["comune"] | null;
consegna: RicercaUrlParams["consegna"] | null;
minBudget: RicercaUrlParams["minBudget"] | null;
maxBudget: RicercaUrlParams["maxBudget"] | null;
sort: RicercaUrlParams["sort"];
map: RicercaUrlParams["map"];
}>,
) => Promise<URLSearchParams>;
reset: () => void;
comuniOptions: string[];
tipologieOptions: string[];
consegnaOptions: number[];
sortOptions: typeof sortOptions;
ResetParams: () => Promise<void>;
nFiltri: number;
isLoading: boolean;
};
const sortOptions = ["Recenti", "Prezzo", "Consegna", "Mq"] as const;
//sort as string union
export type SortTypes = (typeof sortOptions)[number];
export type AnnunciOptions = {
comune: string;
tipo: string;
@ -84,39 +105,36 @@ export const RicercaProvider: FC<{
children: ReactNode;
options: AnnunciOptions;
}> = ({ children, options }) => {
const [nFiltri, setNFiltri] = useState(0);
const [isLoading, startTransition] = useTransition();
const [map, setMap] = useQueryState(
"m",
parseAsBoolean.withDefault(false).withOptions({ startTransition }),
);
const [tipo, setTipo] = useQueryState(
"t",
parseAsString.withDefault("").withOptions({ startTransition }),
);
const [comune, setComune] = useQueryState(
"c",
parseAsString.withDefault("").withOptions({ startTransition }),
);
const [consegna, setConsegna] = useQueryState(
"f",
parseAsArrayOf(parseAsInteger).withOptions({ startTransition }),
);
const [minBudget, setMinBudget] = useQueryState(
"bmin",
parseAsInteger.withOptions({ startTransition }),
);
const [maxBudget, setMaxBudget] = useQueryState(
"bmax",
parseAsInteger.withOptions({ startTransition }),
);
const [sort, setSort] = useQueryState(
"s",
parseAsStringLiteral(sortOptions)
.withDefault("Recenti")
.withOptions({ startTransition }),
const [
{ map, tipo, comune, consegna, minBudget, maxBudget, sort },
setAllParams,
] = useQueryStates(
{
map: parseAsBoolean.withDefault(false),
tipo: parseAsString.withDefault(""),
comune: parseAsString.withDefault(""),
consegna: parseAsArrayOf(parseAsInteger),
minBudget: parseAsInteger,
maxBudget: parseAsInteger,
sort: parseAsStringLiteral(sortOptions).withDefault("Recenti"),
},
{ startTransition, shallow: true, urlKeys: paramsMapping },
);
const reset = useCallback(() => {
setAllParams({
tipo: "",
comune: "",
consegna: null,
minBudget: null,
maxBudget: null,
sort: "Recenti",
});
}, []);
const tipologieOptions = useMemo(() => {
const set = new Set<string>();
options.forEach((item) => {
@ -172,56 +190,43 @@ export const RicercaProvider: FC<{
});
}, [options, tipo, comune, minBudget, maxBudget]);
const ResetParams = async () => {
await setTipo("");
await setComune("");
await setConsegna(null);
await setMinBudget(null);
await setMaxBudget(null);
await setSort("Recenti");
const updateNFiltri = () => {
setNFiltri(
[tipo, comune].reduce(
(count, item) => (item !== "" ? count + 1 : count),
0,
) +
(consegna !== null ? 1 : 0) +
(sort !== "Recenti" ? 1 : 0) +
(minBudget !== null ? 1 : 0) +
(maxBudget !== null ? 1 : 0),
);
};
const nFiltri =
[tipo, comune].reduce(
(count, item) => (item !== "" ? count + 1 : count),
0,
) +
(consegna !== null ? 1 : 0) +
(sort !== "Recenti" ? 1 : 0) +
(minBudget !== null ? 1 : 0) +
(maxBudget !== null ? 1 : 0);
useEffect(() => {
updateNFiltri();
}, [tipo, comune, consegna, minBudget, maxBudget, sort]);
return (
<RicercaContext.Provider
value={{
comune,
setComune,
comuniOptions,
consegna,
setConsegna,
consegnaOptions,
tipo,
setTipo,
tipologieOptions,
minBudget,
setMinBudget,
maxBudget,
setMaxBudget,
sort,
setSort,
sortOptions: sortOptions,
map,
setMap,
tipo,
comune,
consegna,
minBudget,
maxBudget,
sort,
setAllParams,
reset,
comuniOptions,
consegnaOptions,
tipologieOptions,
sortOptions,
nFiltri,
isLoading,
ResetParams,
}}
>
{children}

View file

@ -19,6 +19,7 @@ import { revalidate } from "../utils/revalidationHelper";
// analytics: true,
// });
export type AnnunciWithMedia = Annunci & {
tipo: string;
images: Pick<ImagesRefs, "img" | "thumb">[];
videos: Pick<VideosRefs, "video" | "thumb">[];
};
@ -82,14 +83,15 @@ export const getAnnunciByCod = async ({
.selectAll()
.select((_eb) => [withImages(), withVideos()])
.where("annunci.web", "=", true)
.where("tipo", "is not", null)
.where("codice", "=", cod)
.executeTakeFirst();
if (!annuncio) {
if (!annuncio || annuncio.tipo === null) {
return null;
}
return annuncio;
return annuncio as AnnunciWithMedia;
} catch (e) {
throw new TRPCError({
cause: (e as Error).cause,
@ -151,15 +153,16 @@ export const getAnnunciById_rawImgUrls = async ({
.selectAll()
.select((_eb) => [withImages(), withVideos()])
.where("id", "=", id)
.where("tipo", "is not", null)
.executeTakeFirst();
if (!annuncio) {
if (!annuncio || annuncio.tipo === null) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Annuncio non trovato",
});
}
return annuncio;
return annuncio as AnnunciWithMedia;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",