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:
parent
cb97919157
commit
edaee780d6
22 changed files with 335 additions and 439 deletions
|
|
@ -59,27 +59,27 @@ const OnboardingServizio = ({
|
||||||
<Text className="font-semibold">
|
<Text className="font-semibold">
|
||||||
Annunci selezionati per te:
|
Annunci selezionati per te:
|
||||||
</Text>
|
</Text>
|
||||||
<Section className="mb-[30px]" key={annuncio.codice}>
|
<Section className="mb-7.5" key={annuncio.codice}>
|
||||||
<Row className="mb-[12px]">
|
<Row className="mb-3">
|
||||||
<Img
|
<Img
|
||||||
alt={`Annuncio image - ${annuncio.codice}`}
|
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"
|
height="140px"
|
||||||
src={`${env.BASE_URL}/storage-api/get/${annuncio.immagine}?media=image`}
|
src={`${env.BASE_URL}/storage-api/get/${annuncio.immagine}?media=image`}
|
||||||
width="100%"
|
width="100%"
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="block w-full">
|
<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}
|
<strong>Codice: </strong> {annuncio.codice}
|
||||||
</div>
|
</div>
|
||||||
<Heading
|
<Heading
|
||||||
as="h2"
|
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"}
|
{annuncio.titolo || "N/A"}
|
||||||
</Heading>
|
</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> €
|
<strong>Prezzo:</strong> €
|
||||||
{(annuncio.prezzo / 1e2).toFixed(2)}
|
{(annuncio.prezzo / 1e2).toFixed(2)}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -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&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>
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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 [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -53,14 +53,14 @@ const MapDisplay = memo(
|
||||||
selectMaxBudget,
|
selectMaxBudget,
|
||||||
}: {
|
}: {
|
||||||
selectTipo: string;
|
selectTipo: string;
|
||||||
selectComune: string;
|
selectComune: string | null;
|
||||||
selectConsegna: number[] | null;
|
selectConsegna: number[] | null;
|
||||||
setSelected: Dispatch<SetStateAction<AnnuncioRicercaWPosition | null>>;
|
setSelected: Dispatch<SetStateAction<AnnuncioRicercaWPosition | null>>;
|
||||||
selectMinBudget: number | null;
|
selectMinBudget: number | null;
|
||||||
selectMaxBudget: number | null;
|
selectMaxBudget: number | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading } = api.annunci.getMapping.useQuery({
|
const { data, isLoading } = api.annunci.getMapping.useQuery({
|
||||||
comune: selectComune,
|
comune: selectComune || undefined,
|
||||||
consegna: selectConsegna || undefined,
|
consegna: selectConsegna || undefined,
|
||||||
tipologia: selectTipo,
|
tipologia: selectTipo,
|
||||||
minBudget: selectMinBudget || undefined,
|
minBudget: selectMinBudget || undefined,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "~/components/ui/dialog";
|
} from "~/components/ui/dialog";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import { buildRicercaUrl } from "~/providers/RicercaProvider";
|
||||||
|
|
||||||
const COOKIE_KEY = "tipologia_tutorial_shown";
|
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
|
expires: add(new Date(), { days: 30 }), // Set cookie to expire in 30 days
|
||||||
});
|
});
|
||||||
if (v !== null) {
|
if (v !== null) {
|
||||||
window.location.assign(`/annunci?tipo=${v}`);
|
window.location.assign(buildRicercaUrl({ tipo: v }));
|
||||||
}
|
}
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ const SkeletonCardAnnuncio = () => {
|
||||||
<Skeleton className="h-10 w-full rounded-md" />
|
<Skeleton className="h-10 w-full rounded-md" />
|
||||||
<Skeleton className="h-10 w-full rounded-md" />
|
<Skeleton className="h-10 w-full rounded-md" />
|
||||||
</div>
|
</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" />
|
<Skeleton className="h-20 w-full rounded-md" />
|
||||||
|
|
||||||
<div className="flex h-9 items-center justify-around gap-1.5">
|
<div className="flex h-9 items-center justify-around gap-1.5">
|
||||||
|
|
@ -451,7 +451,7 @@ export const CarouselAnnuncio = ({
|
||||||
<Image
|
<Image
|
||||||
alt={img.img}
|
alt={img.img}
|
||||||
className={cn(
|
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",
|
immagini.length === 1 && "object-contain",
|
||||||
)}
|
)}
|
||||||
height={400}
|
height={400}
|
||||||
|
|
@ -484,7 +484,7 @@ export const CarouselAnnuncio = ({
|
||||||
key={`videoplayer-${video.video}`}
|
key={`videoplayer-${video.video}`}
|
||||||
>
|
>
|
||||||
<VideoPlayer
|
<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({
|
coverImage={getStorageUrl({
|
||||||
storageId: video.thumb,
|
storageId: video.thumb,
|
||||||
params: {
|
params: {
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ const AutocompleteSearchBox = ({
|
||||||
}, [searchValue, options]);
|
}, [searchValue, options]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-[5000] w-full" ref={wrapperRef}>
|
<div className="z-5000 w-full" ref={wrapperRef}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full items-center justify-center rounded-md bg-secondary dark:bg-primary",
|
"relative flex w-full items-center justify-center rounded-md bg-secondary dark:bg-primary",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function FailedAnnuncioLoading() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<main>
|
<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">
|
<div className="mx-auto max-w-lg space-y-3 text-center">
|
||||||
<HomeIcon className="mx-auto size-20 text-destructive" />
|
<HomeIcon className="mx-auto size-20 text-destructive" />
|
||||||
<h3 className="font-semibold text-4xl text-accent-foreground sm:text-5xl">
|
<h3 className="font-semibold text-4xl text-accent-foreground sm:text-5xl">
|
||||||
|
|
|
||||||
97
apps/infoalloggi/src/components/frequent_searches.tsx
Normal file
97
apps/infoalloggi/src/components/frequent_searches.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -35,7 +35,7 @@ export const AnnunciCompatibili = () => {
|
||||||
Esplora Annunci Compatibili
|
Esplora Annunci Compatibili
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</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">
|
<DialogHeader className="flex-row flex-wrap items-center gap-5 pt-6">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Annunci compatibili con i parametri del servizio
|
Annunci compatibili con i parametri del servizio
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ export const AnnuncioEditForm = ({ data }: { data: AnnunciWithMedia }) => {
|
||||||
stato: z.string().nullable(),
|
stato: z.string().nullable(),
|
||||||
|
|
||||||
// DATI CONTRATTO
|
// DATI CONTRATTO
|
||||||
tipo: z.string().nullable(),
|
tipo: z.string(),
|
||||||
titolo_en: z.string().nullable(),
|
titolo_en: z.string().nullable(),
|
||||||
titolo_it: z.string().nullable(),
|
titolo_it: z.string().nullable(),
|
||||||
unita_condominio: z.number().nullable(),
|
unita_condominio: z.number().nullable(),
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@ export type LangDict = {
|
||||||
endtxt: string;
|
endtxt: string;
|
||||||
endtxt2: string;
|
endtxt2: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
annunci: {
|
annunci: {
|
||||||
titolo: string;
|
titolo: string;
|
||||||
fs_tipologia: {
|
fs_tipologia: {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default function Custom500Auth() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<main>
|
<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">
|
<div className="mx-auto max-w-lg space-y-3 text-center">
|
||||||
<ShieldExclamationIcon className="mx-auto size-20 text-red-600" />
|
<ShieldExclamationIcon className="mx-auto size-20 text-red-600" />
|
||||||
<h3 className="font-semibold text-4xl text-accent-foreground sm:text-5xl">
|
<h3 className="font-semibold text-4xl text-accent-foreground sm:text-5xl">
|
||||||
|
|
|
||||||
|
|
@ -105,14 +105,17 @@ export const getServerSideProps = (async () => {
|
||||||
}) satisfies GetServerSideProps<AnnunciPageProps>;
|
}) satisfies GetServerSideProps<AnnunciPageProps>;
|
||||||
|
|
||||||
const Ricerca = () => {
|
const Ricerca = () => {
|
||||||
const { map } = useRicerca();
|
const { map, isLoading } = useRicerca();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full space-y-5">
|
<div className="mx-auto w-full space-y-5">
|
||||||
<RicercaHeader />
|
<RicercaHeader />
|
||||||
<div className="relative flex w-full gap-4">
|
<div className="relative flex w-full gap-4">
|
||||||
<RicercaSideFilters />
|
<RicercaSideFilters />
|
||||||
<div className="flex-1">{map ? <MapSection /> : <AnnunciList />}</div>
|
|
||||||
|
<div className="flex-1">
|
||||||
|
{isLoading ? <LoadingPage /> : map ? <MapSection /> : <AnnunciList />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -122,14 +125,12 @@ const Filters = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
tipo,
|
tipo,
|
||||||
setTipo,
|
|
||||||
comune,
|
comune,
|
||||||
setComune,
|
|
||||||
sort,
|
sort,
|
||||||
setSort,
|
|
||||||
comuniOptions,
|
comuniOptions,
|
||||||
tipologieOptions,
|
tipologieOptions,
|
||||||
ResetParams,
|
setAllParams,
|
||||||
|
reset,
|
||||||
nFiltri,
|
nFiltri,
|
||||||
} = useRicerca();
|
} = useRicerca();
|
||||||
|
|
||||||
|
|
@ -138,39 +139,55 @@ const Filters = () => {
|
||||||
<SelectFilter
|
<SelectFilter
|
||||||
defaultValue={""}
|
defaultValue={""}
|
||||||
handleReset={async () => {
|
handleReset={async () => {
|
||||||
await setTipo("");
|
await setAllParams({
|
||||||
|
tipo: "",
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
|
id="tipologia-filter"
|
||||||
label={t.annunci.fs_tipologia.titolo}
|
label={t.annunci.fs_tipologia.titolo}
|
||||||
onValueChange={async (value: string) => {
|
onValueChange={async (value: string) => {
|
||||||
await setTipo(value);
|
await setAllParams({
|
||||||
|
tipo: value,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
options={tipologieOptions}
|
options={tipologieOptions}
|
||||||
placeholder={t.seleziona_placeholder}
|
placeholder={t.seleziona_placeholder}
|
||||||
value={tipo}
|
value={tipo}
|
||||||
/>
|
/>
|
||||||
<SelectFilter
|
<SelectFilter
|
||||||
defaultValue={""}
|
defaultValue={undefined}
|
||||||
handleReset={async () => {
|
handleReset={async () => {
|
||||||
await setComune(null);
|
await setAllParams({
|
||||||
|
comune: null,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
|
id="comune-filter"
|
||||||
|
key={comune}
|
||||||
label={t.annunci.comune}
|
label={t.annunci.comune}
|
||||||
onValueChange={async (value: string) => {
|
onValueChange={async (value: string) => {
|
||||||
await setComune(value);
|
await setAllParams({
|
||||||
|
comune: value,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
options={comuniOptions}
|
options={comuniOptions}
|
||||||
placeholder={t.seleziona_placeholder}
|
placeholder={t.seleziona_placeholder}
|
||||||
value={comune}
|
value={comune || undefined}
|
||||||
/>
|
/>
|
||||||
<ConsegnaFilter />
|
<ConsegnaFilter />
|
||||||
<BudgetFilters />
|
<BudgetFilters />
|
||||||
<SelectFilter
|
<SelectFilter
|
||||||
defaultValue={sort}
|
defaultValue={sort}
|
||||||
handleReset={async () => {
|
handleReset={async () => {
|
||||||
await setSort("Recenti");
|
await setAllParams({
|
||||||
|
sort: "Recenti",
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
|
id="sort-filter"
|
||||||
label={t.ordina_per}
|
label={t.ordina_per}
|
||||||
onValueChange={async (value: SortTypes) => {
|
onValueChange={async (value: SortTypes) => {
|
||||||
await setSort(value);
|
await setAllParams({
|
||||||
|
sort: value,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
options={["Recenti", "Prezzo", "Consegna", "Mq"]}
|
options={["Recenti", "Prezzo", "Consegna", "Mq"]}
|
||||||
placeholder={t.seleziona_placeholder}
|
placeholder={t.seleziona_placeholder}
|
||||||
|
|
@ -179,7 +196,7 @@ const Filters = () => {
|
||||||
<Button
|
<Button
|
||||||
className="flex flex-row gap-2"
|
className="flex flex-row gap-2"
|
||||||
disabled={nFiltri === 0}
|
disabled={nFiltri === 0}
|
||||||
onClick={ResetParams}
|
onClick={reset}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
>
|
>
|
||||||
<FilterX /> {t.annunci.filtri_reset}
|
<FilterX /> {t.annunci.filtri_reset}
|
||||||
|
|
@ -189,8 +206,11 @@ const Filters = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const RicercaSideFilters = () => {
|
const RicercaSideFilters = () => {
|
||||||
|
const isBigScreen = useMediaQuery("(min-width: 96rem)");
|
||||||
|
|
||||||
|
if (!isBigScreen) return null;
|
||||||
return (
|
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 />
|
<Filters />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -230,7 +250,7 @@ const DrowdownFilters = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const RicercaHeader = () => {
|
const RicercaHeader = () => {
|
||||||
const { map, setMap } = useRicerca();
|
const { map, setAllParams } = useRicerca();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-10xl space-y-5">
|
<div className="mx-auto w-full max-w-10xl space-y-5">
|
||||||
|
|
@ -240,7 +260,9 @@ const RicercaHeader = () => {
|
||||||
<Button
|
<Button
|
||||||
className="flex w-fit flex-row items-center gap-2 md:w-24"
|
className="flex w-fit flex-row items-center gap-2 md:w-24"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await setMap((v) => !v);
|
await setAllParams({
|
||||||
|
map: !map,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
|
|
@ -281,10 +303,10 @@ const AnnunciList = () => {
|
||||||
map,
|
map,
|
||||||
minBudget,
|
minBudget,
|
||||||
maxBudget,
|
maxBudget,
|
||||||
isLoading: ricercaLoading,
|
isLoading: isUpdatingRicerca,
|
||||||
} = useRicerca();
|
} = useRicerca();
|
||||||
|
|
||||||
const { data, status } = api.annunci.annunciRicerca.useQuery(
|
const { data, status, isLoading } = api.annunci.annunciRicerca.useQuery(
|
||||||
{
|
{
|
||||||
comune: comune || undefined,
|
comune: comune || undefined,
|
||||||
consegna: consegna || undefined,
|
consegna: consegna || undefined,
|
||||||
|
|
@ -294,13 +316,13 @@ const AnnunciList = () => {
|
||||||
maxBudget: maxBudget || undefined,
|
maxBudget: maxBudget || undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !map,
|
enabled: !map && !isUpdatingRicerca,
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
staleTime: 5000,
|
staleTime: 5000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ricercaLoading) return <LoadingPage />;
|
if (isLoading) return <LoadingPage />;
|
||||||
if (status === "error") return <Status500 />;
|
if (status === "error") return <Status500 />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -325,6 +347,7 @@ const AnnunciList = () => {
|
||||||
|
|
||||||
interface SelectFilterProps<T> {
|
interface SelectFilterProps<T> {
|
||||||
label: string;
|
label: string;
|
||||||
|
id: string;
|
||||||
options: string[];
|
options: string[];
|
||||||
defaultValue?: T;
|
defaultValue?: T;
|
||||||
value: T | undefined;
|
value: T | undefined;
|
||||||
|
|
@ -335,6 +358,7 @@ interface SelectFilterProps<T> {
|
||||||
|
|
||||||
export const SelectFilter = <T extends string>({
|
export const SelectFilter = <T extends string>({
|
||||||
label,
|
label,
|
||||||
|
id,
|
||||||
options,
|
options,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
value,
|
value,
|
||||||
|
|
@ -350,6 +374,7 @@ export const SelectFilter = <T extends string>({
|
||||||
"font-semibold text-muted-foreground",
|
"font-semibold text-muted-foreground",
|
||||||
value !== defaultValue && "text-primary",
|
value !== defaultValue && "text-primary",
|
||||||
)}
|
)}
|
||||||
|
htmlFor={id}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -369,7 +394,10 @@ export const SelectFilter = <T extends string>({
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
value={value}
|
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} />
|
<SelectValue placeholder={placeholder} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
@ -386,12 +414,7 @@ export const SelectFilter = <T extends string>({
|
||||||
|
|
||||||
const ConsegnaFilter = () => {
|
const ConsegnaFilter = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const { consegna: consegna2, setAllParams, consegnaOptions } = useRicerca();
|
||||||
consegna: consegna2,
|
|
||||||
setConsegna: setConsegna2,
|
|
||||||
|
|
||||||
consegnaOptions,
|
|
||||||
} = useRicerca();
|
|
||||||
|
|
||||||
const mappedConsegnaOptions = useMemo(() => {
|
const mappedConsegnaOptions = useMemo(() => {
|
||||||
if (consegnaOptions.length === 0) return [];
|
if (consegnaOptions.length === 0) return [];
|
||||||
|
|
@ -404,14 +427,17 @@ const ConsegnaFilter = () => {
|
||||||
|
|
||||||
const onValueChange = async (value: number) => {
|
const onValueChange = async (value: number) => {
|
||||||
if (consegna2?.includes(value)) {
|
if (consegna2?.includes(value)) {
|
||||||
// Remove value
|
|
||||||
const newConsegna2 = consegna2.filter((c) => c !== value);
|
const newConsegna2 = consegna2.filter((c) => c !== value);
|
||||||
await setConsegna2(newConsegna2.length > 0 ? newConsegna2 : null);
|
await setAllParams({
|
||||||
|
consegna: newConsegna2.length > 0 ? newConsegna2 : null,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Add value
|
|
||||||
const newConsegna2 = consegna2 ? [...consegna2, value] : [value];
|
const newConsegna2 = consegna2 ? [...consegna2, value] : [value];
|
||||||
await setConsegna2(newConsegna2);
|
await setAllParams({
|
||||||
|
consegna: newConsegna2,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|
@ -429,7 +455,9 @@ const ConsegnaFilter = () => {
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await setConsegna2(null);
|
await setAllParams({
|
||||||
|
consegna: null,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
@ -461,20 +489,24 @@ const ConsegnaFilter = () => {
|
||||||
|
|
||||||
const BudgetFilters = () => {
|
const BudgetFilters = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { minBudget, setMinBudget, maxBudget, setMaxBudget } = useRicerca();
|
const { minBudget, maxBudget, setAllParams } = useRicerca();
|
||||||
|
|
||||||
const minDebounced = useCallback(
|
const minDebounced = useCallback(
|
||||||
debounce(async (value: number | null) => {
|
debounce(async (value: number | null) => {
|
||||||
await setMinBudget(value);
|
await setAllParams({
|
||||||
|
minBudget: value,
|
||||||
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
[setMinBudget],
|
[setAllParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
const maxDebounced = useCallback(
|
const maxDebounced = useCallback(
|
||||||
debounce(async (value: number | null) => {
|
debounce(async (value: number | null) => {
|
||||||
await setMaxBudget(value);
|
await setAllParams({
|
||||||
|
maxBudget: value,
|
||||||
|
});
|
||||||
}, 500),
|
}, 500),
|
||||||
[setMaxBudget],
|
[setAllParams],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -486,6 +518,7 @@ const BudgetFilters = () => {
|
||||||
"font-semibold text-muted-foreground",
|
"font-semibold text-muted-foreground",
|
||||||
minBudget !== null && "text-primary",
|
minBudget !== null && "text-primary",
|
||||||
)}
|
)}
|
||||||
|
htmlFor="prezzoMIN"
|
||||||
>
|
>
|
||||||
{t.annunci.budgetMin}
|
{t.annunci.budgetMin}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -493,7 +526,9 @@ const BudgetFilters = () => {
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await setMinBudget(null);
|
await setAllParams({
|
||||||
|
minBudget: null,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
@ -507,7 +542,7 @@ const BudgetFilters = () => {
|
||||||
decimalScale={2}
|
decimalScale={2}
|
||||||
decimalSeparator=","
|
decimalSeparator=","
|
||||||
fixedDecimalScale
|
fixedDecimalScale
|
||||||
id="prezzo"
|
id="prezzoMIN"
|
||||||
min={0}
|
min={0}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
if (v && v > 0) {
|
if (v && v > 0) {
|
||||||
|
|
@ -529,6 +564,7 @@ const BudgetFilters = () => {
|
||||||
"font-semibold text-muted-foreground",
|
"font-semibold text-muted-foreground",
|
||||||
maxBudget !== null && "text-primary",
|
maxBudget !== null && "text-primary",
|
||||||
)}
|
)}
|
||||||
|
htmlFor="prezzoMAX"
|
||||||
>
|
>
|
||||||
{t.annunci.budgetMax}
|
{t.annunci.budgetMax}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
@ -536,7 +572,9 @@ const BudgetFilters = () => {
|
||||||
<button
|
<button
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await setMaxBudget(null);
|
await setAllParams({
|
||||||
|
maxBudget: null,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
@ -549,7 +587,7 @@ const BudgetFilters = () => {
|
||||||
decimalScale={2}
|
decimalScale={2}
|
||||||
decimalSeparator=","
|
decimalSeparator=","
|
||||||
fixedDecimalScale
|
fixedDecimalScale
|
||||||
id="prezzo"
|
id="prezzoMAX"
|
||||||
min={0}
|
min={0}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
if (v && v > 0) {
|
if (v && v > 0) {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ import { getStorageUrl } from "~/lib/storage_utils";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { AnnuncioContext } from "~/providers/AnnuncioProvider";
|
import { AnnuncioContext } from "~/providers/AnnuncioProvider";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
import { buildRicercaUrl } from "~/providers/RicercaProvider";
|
||||||
import { useSession } from "~/providers/SessionProvider";
|
import { useSession } from "~/providers/SessionProvider";
|
||||||
import type { Annunci } from "~/schemas/public/Annunci";
|
import type { Annunci } from "~/schemas/public/Annunci";
|
||||||
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
|
||||||
|
|
@ -659,7 +660,7 @@ const AnnuncioFooter = ({
|
||||||
external_videos,
|
external_videos,
|
||||||
updated_at,
|
updated_at,
|
||||||
}: {
|
}: {
|
||||||
tipo: string | null;
|
tipo: string;
|
||||||
comune: string | null;
|
comune: string | null;
|
||||||
consegna: number | null;
|
consegna: number | null;
|
||||||
videos: Pick<VideosRefs, "video" | "thumb">[];
|
videos: Pick<VideosRefs, "video" | "thumb">[];
|
||||||
|
|
@ -706,14 +707,14 @@ const AnnuncioFooter = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={{
|
href={buildRicercaUrl({
|
||||||
pathname: "/annunci",
|
comune: comune || undefined,
|
||||||
query: {
|
consegna: consegna ? [consegna] : undefined,
|
||||||
c: comune,
|
tipo:
|
||||||
f: consegna,
|
tipo in ["Transitorio", "Stabile"]
|
||||||
t: tipo,
|
? (tipo as "Transitorio" | "Stabile")
|
||||||
},
|
: undefined,
|
||||||
}}
|
})}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<Button className="my-10 w-full text-xl" variant="secondary">
|
<Button className="my-10 w-full text-xl" variant="secondary">
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import { ArrowRight, Telescope } from "lucide-react";
|
import { Telescope } from "lucide-react";
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { AccordionComp } from "~/components/accordionComp";
|
import { AccordionComp } from "~/components/accordionComp";
|
||||||
import { CodiceBox } from "~/components/codiceRicerca";
|
import { CodiceBox } from "~/components/codiceRicerca";
|
||||||
import { ComeFunziona } from "~/components/expand_guida";
|
import { ComeFunziona } from "~/components/expand_guida";
|
||||||
|
import { FrequentSearches } from "~/components/frequent_searches";
|
||||||
import { PricingChoice } from "~/components/prezzi";
|
import { PricingChoice } from "~/components/prezzi";
|
||||||
import { HeroSvg, LogoSvg } from "~/components/svgs";
|
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 { env } from "~/env";
|
||||||
import type { LangDict } from "~/i18n/locales";
|
import type { LangDict } from "~/i18n/locales";
|
||||||
import { useTranslation } from "~/providers/I18nProvider";
|
import { useTranslation } from "~/providers/I18nProvider";
|
||||||
|
|
@ -37,11 +36,11 @@ const Home: NextPage = () => {
|
||||||
/>
|
/>
|
||||||
<meta content="Infoalloggi.it" property="og:site_name" />
|
<meta content="Infoalloggi.it" property="og:site_name" />
|
||||||
</Head>
|
</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">
|
<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="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="-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 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="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 />
|
<HeroSvg />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -77,7 +76,7 @@ const TrovaCasaCTA = (props: { testi: LangDict }) => {
|
||||||
<span className="text-destructive"> {testi.index.titolo_2}</span>{" "}
|
<span className="text-destructive"> {testi.index.titolo_2}</span>{" "}
|
||||||
{testi.index.titolo_3}
|
{testi.index.titolo_3}
|
||||||
</h1>
|
</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 />
|
<br />
|
||||||
|
|
||||||
<div className="flex flex-col space-y-4 lg:justify-start">
|
<div className="flex flex-col space-y-4 lg:justify-start">
|
||||||
|
|
@ -106,68 +105,3 @@ const TrovaCasaCTA = (props: { testi: LangDict }) => {
|
||||||
</div>
|
</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import { env } from "~/env";
|
import { env } from "~/env";
|
||||||
|
import { buildRicercaUrl } from "~/providers/RicercaProvider";
|
||||||
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
import { generateSSGHelper } from "~/server/utils/ssgHelper";
|
||||||
|
|
||||||
async function generateSiteMap() {
|
async function generateSiteMap() {
|
||||||
|
|
@ -13,10 +14,11 @@ async function generateSiteMap() {
|
||||||
`${env.NEXT_PUBLIC_BASE_URL}/proprietari`,
|
`${env.NEXT_PUBLIC_BASE_URL}/proprietari`,
|
||||||
`${env.NEXT_PUBLIC_BASE_URL}/guida`,
|
`${env.NEXT_PUBLIC_BASE_URL}/guida`,
|
||||||
`${env.NEXT_PUBLIC_BASE_URL}/annunci`,
|
`${env.NEXT_PUBLIC_BASE_URL}/annunci`,
|
||||||
`${env.NEXT_PUBLIC_BASE_URL}/annunci?t=Transitorio`,
|
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ tipo: "Transitorio" })}`,
|
||||||
`${env.NEXT_PUBLIC_BASE_URL}/annunci?c=Bassano+del+Grappa`,
|
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ comune: "Bassano del Grappa" })}`,
|
||||||
`${env.NEXT_PUBLIC_BASE_URL}/annunci?t=Stabile`,
|
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ comune: "Bassano del Grappa", tipo: "Transitorio" })}`,
|
||||||
`${env.NEXT_PUBLIC_BASE_URL}/annunci?c=Marostica`,
|
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ tipo: "Stabile" })}`,
|
||||||
|
`${env.NEXT_PUBLIC_BASE_URL}${buildRicercaUrl({ comune: "Marostica" })}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
const allUrls = [...staticUrls, ...dynamicUrls];
|
const allUrls = [...staticUrls, ...dynamicUrls];
|
||||||
|
|
|
||||||
|
|
@ -5,64 +5,85 @@ import {
|
||||||
parseAsInteger,
|
parseAsInteger,
|
||||||
parseAsString,
|
parseAsString,
|
||||||
parseAsStringLiteral,
|
parseAsStringLiteral,
|
||||||
useQueryState,
|
useQueryStates,
|
||||||
} from "nuqs";
|
} from "nuqs";
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
type FC,
|
type FC,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
useContext,
|
useContext,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useState,
|
||||||
useTransition,
|
useTransition,
|
||||||
} from "react";
|
} 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 = {
|
type RicercaContextType = {
|
||||||
map: boolean;
|
map: boolean;
|
||||||
setMap: (
|
tipo: RicercaUrlParams["tipo"];
|
||||||
value: boolean | ((old: boolean) => boolean | null) | null,
|
comune: RicercaUrlParams["comune"] | null;
|
||||||
) => Promise<URLSearchParams>;
|
consegna: RicercaUrlParams["consegna"] | null;
|
||||||
tipo: string;
|
minBudget: RicercaUrlParams["minBudget"] | null;
|
||||||
setTipo: (
|
maxBudget: RicercaUrlParams["maxBudget"] | null;
|
||||||
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>;
|
|
||||||
|
|
||||||
sort: SortTypes;
|
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>;
|
) => Promise<URLSearchParams>;
|
||||||
|
reset: () => void;
|
||||||
|
|
||||||
comuniOptions: string[];
|
comuniOptions: string[];
|
||||||
tipologieOptions: string[];
|
tipologieOptions: string[];
|
||||||
consegnaOptions: number[];
|
consegnaOptions: number[];
|
||||||
sortOptions: typeof sortOptions;
|
sortOptions: typeof sortOptions;
|
||||||
ResetParams: () => Promise<void>;
|
|
||||||
|
|
||||||
nFiltri: number;
|
nFiltri: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortOptions = ["Recenti", "Prezzo", "Consegna", "Mq"] as const;
|
|
||||||
|
|
||||||
//sort as string union
|
|
||||||
export type SortTypes = (typeof sortOptions)[number];
|
|
||||||
|
|
||||||
export type AnnunciOptions = {
|
export type AnnunciOptions = {
|
||||||
comune: string;
|
comune: string;
|
||||||
tipo: string;
|
tipo: string;
|
||||||
|
|
@ -84,39 +105,36 @@ export const RicercaProvider: FC<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
options: AnnunciOptions;
|
options: AnnunciOptions;
|
||||||
}> = ({ children, options }) => {
|
}> = ({ children, options }) => {
|
||||||
|
const [nFiltri, setNFiltri] = useState(0);
|
||||||
const [isLoading, startTransition] = useTransition();
|
const [isLoading, startTransition] = useTransition();
|
||||||
|
|
||||||
const [map, setMap] = useQueryState(
|
const [
|
||||||
"m",
|
{ map, tipo, comune, consegna, minBudget, maxBudget, sort },
|
||||||
parseAsBoolean.withDefault(false).withOptions({ startTransition }),
|
setAllParams,
|
||||||
);
|
] = useQueryStates(
|
||||||
const [tipo, setTipo] = useQueryState(
|
{
|
||||||
"t",
|
map: parseAsBoolean.withDefault(false),
|
||||||
parseAsString.withDefault("").withOptions({ startTransition }),
|
tipo: parseAsString.withDefault(""),
|
||||||
);
|
comune: parseAsString.withDefault(""),
|
||||||
const [comune, setComune] = useQueryState(
|
consegna: parseAsArrayOf(parseAsInteger),
|
||||||
"c",
|
minBudget: parseAsInteger,
|
||||||
parseAsString.withDefault("").withOptions({ startTransition }),
|
maxBudget: parseAsInteger,
|
||||||
);
|
sort: parseAsStringLiteral(sortOptions).withDefault("Recenti"),
|
||||||
const [consegna, setConsegna] = useQueryState(
|
},
|
||||||
"f",
|
{ startTransition, shallow: true, urlKeys: paramsMapping },
|
||||||
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 reset = useCallback(() => {
|
||||||
|
setAllParams({
|
||||||
|
tipo: "",
|
||||||
|
comune: "",
|
||||||
|
consegna: null,
|
||||||
|
minBudget: null,
|
||||||
|
maxBudget: null,
|
||||||
|
sort: "Recenti",
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const tipologieOptions = useMemo(() => {
|
const tipologieOptions = useMemo(() => {
|
||||||
const set = new Set<string>();
|
const set = new Set<string>();
|
||||||
options.forEach((item) => {
|
options.forEach((item) => {
|
||||||
|
|
@ -172,16 +190,8 @@ export const RicercaProvider: FC<{
|
||||||
});
|
});
|
||||||
}, [options, tipo, comune, minBudget, maxBudget]);
|
}, [options, tipo, comune, minBudget, maxBudget]);
|
||||||
|
|
||||||
const ResetParams = async () => {
|
const updateNFiltri = () => {
|
||||||
await setTipo("");
|
setNFiltri(
|
||||||
await setComune("");
|
|
||||||
await setConsegna(null);
|
|
||||||
await setMinBudget(null);
|
|
||||||
await setMaxBudget(null);
|
|
||||||
await setSort("Recenti");
|
|
||||||
};
|
|
||||||
|
|
||||||
const nFiltri =
|
|
||||||
[tipo, comune].reduce(
|
[tipo, comune].reduce(
|
||||||
(count, item) => (item !== "" ? count + 1 : count),
|
(count, item) => (item !== "" ? count + 1 : count),
|
||||||
0,
|
0,
|
||||||
|
|
@ -189,39 +199,34 @@ export const RicercaProvider: FC<{
|
||||||
(consegna !== null ? 1 : 0) +
|
(consegna !== null ? 1 : 0) +
|
||||||
(sort !== "Recenti" ? 1 : 0) +
|
(sort !== "Recenti" ? 1 : 0) +
|
||||||
(minBudget !== null ? 1 : 0) +
|
(minBudget !== null ? 1 : 0) +
|
||||||
(maxBudget !== null ? 1 : 0);
|
(maxBudget !== null ? 1 : 0),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateNFiltri();
|
||||||
|
}, [tipo, comune, consegna, minBudget, maxBudget, sort]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RicercaContext.Provider
|
<RicercaContext.Provider
|
||||||
value={{
|
value={{
|
||||||
comune,
|
|
||||||
setComune,
|
|
||||||
comuniOptions,
|
|
||||||
|
|
||||||
consegna,
|
|
||||||
setConsegna,
|
|
||||||
consegnaOptions,
|
|
||||||
|
|
||||||
tipo,
|
|
||||||
setTipo,
|
|
||||||
tipologieOptions,
|
|
||||||
|
|
||||||
minBudget,
|
|
||||||
setMinBudget,
|
|
||||||
|
|
||||||
maxBudget,
|
|
||||||
setMaxBudget,
|
|
||||||
|
|
||||||
sort,
|
|
||||||
setSort,
|
|
||||||
sortOptions: sortOptions,
|
|
||||||
|
|
||||||
map,
|
map,
|
||||||
setMap,
|
tipo,
|
||||||
|
comune,
|
||||||
|
consegna,
|
||||||
|
minBudget,
|
||||||
|
maxBudget,
|
||||||
|
sort,
|
||||||
|
|
||||||
|
setAllParams,
|
||||||
|
reset,
|
||||||
|
|
||||||
|
comuniOptions,
|
||||||
|
consegnaOptions,
|
||||||
|
tipologieOptions,
|
||||||
|
sortOptions,
|
||||||
nFiltri,
|
nFiltri,
|
||||||
isLoading,
|
isLoading,
|
||||||
ResetParams,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { revalidate } from "../utils/revalidationHelper";
|
||||||
// analytics: true,
|
// analytics: true,
|
||||||
// });
|
// });
|
||||||
export type AnnunciWithMedia = Annunci & {
|
export type AnnunciWithMedia = Annunci & {
|
||||||
|
tipo: string;
|
||||||
images: Pick<ImagesRefs, "img" | "thumb">[];
|
images: Pick<ImagesRefs, "img" | "thumb">[];
|
||||||
videos: Pick<VideosRefs, "video" | "thumb">[];
|
videos: Pick<VideosRefs, "video" | "thumb">[];
|
||||||
};
|
};
|
||||||
|
|
@ -82,14 +83,15 @@ export const getAnnunciByCod = async ({
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.select((_eb) => [withImages(), withVideos()])
|
.select((_eb) => [withImages(), withVideos()])
|
||||||
.where("annunci.web", "=", true)
|
.where("annunci.web", "=", true)
|
||||||
|
.where("tipo", "is not", null)
|
||||||
.where("codice", "=", cod)
|
.where("codice", "=", cod)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (!annuncio) {
|
if (!annuncio || annuncio.tipo === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return annuncio;
|
return annuncio as AnnunciWithMedia;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
cause: (e as Error).cause,
|
cause: (e as Error).cause,
|
||||||
|
|
@ -151,15 +153,16 @@ export const getAnnunciById_rawImgUrls = async ({
|
||||||
.selectAll()
|
.selectAll()
|
||||||
.select((_eb) => [withImages(), withVideos()])
|
.select((_eb) => [withImages(), withVideos()])
|
||||||
.where("id", "=", id)
|
.where("id", "=", id)
|
||||||
|
.where("tipo", "is not", null)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
if (!annuncio) {
|
if (!annuncio || annuncio.tipo === null) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "Annuncio non trovato",
|
message: "Annuncio non trovato",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return annuncio;
|
return annuncio as AnnunciWithMedia;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue