feat: update Servizio component to handle interruzioneDays and acceptedInterr logic refactor: simplify servizio_actions by removing unused InterruzioneServizio and EditParametri components feat: enhance FormEditServizioAdmin to include interruzioneDays and acceptedInterr fields fix: adjust email service to delete obsolete emails and handle invalid email types gracefully chore: update stripe controller to utilize interruzioneDays for lock expiration logic fix: ensure correct handling of servizio expiration and interruzione logic in service controller
177 lines
4.9 KiB
TypeScript
177 lines
4.9 KiB
TypeScript
import { AnimatePresence, motion } from "framer-motion";
|
|
import dynamic from "next/dynamic";
|
|
import Image from "next/image";
|
|
import {
|
|
type Dispatch,
|
|
Fragment,
|
|
memo,
|
|
type SetStateAction,
|
|
useState,
|
|
} from "react";
|
|
import { LoadingPage } from "~/components/loading";
|
|
import { Status500 } from "~/components/status-page";
|
|
import { handleConsegna } from "~/lib/annuncio_details";
|
|
import { getStorageUrl } from "~/lib/storage_utils";
|
|
import { formatCurrency } from "~/lib/utils";
|
|
import { useTranslation } from "~/providers/I18nProvider";
|
|
import { useRicerca } from "~/providers/RicercaProvider";
|
|
import type { AnnuncioRicercaWPosition } from "~/server/controllers/annunci.controller";
|
|
import { api } from "~/utils/api";
|
|
|
|
const MapComp = dynamic(() => import("~/components/map/Map"), {
|
|
ssr: false,
|
|
});
|
|
|
|
export const MapSection = () => {
|
|
const { comune, consegna, tipo, minBudget, maxBudget } = useRicerca();
|
|
const [selected, setSelected] = useState<AnnuncioRicercaWPosition | null>(
|
|
null,
|
|
);
|
|
|
|
return (
|
|
<div className="relative z-30 flex h-[60vh] w-full items-center justify-center sm:h-[65vh]">
|
|
<MapDisplay
|
|
selectComune={comune}
|
|
selectConsegna={consegna}
|
|
selectMaxBudget={maxBudget}
|
|
selectMinBudget={minBudget}
|
|
selectTipo={tipo}
|
|
setSelected={setSelected}
|
|
/>
|
|
<SelectedComp selected={selected} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const MapDisplay = memo(
|
|
({
|
|
selectTipo,
|
|
selectComune,
|
|
selectConsegna,
|
|
setSelected,
|
|
selectMinBudget,
|
|
selectMaxBudget,
|
|
}: {
|
|
selectTipo: 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 || undefined,
|
|
consegna: selectConsegna || undefined,
|
|
tipologia: selectTipo,
|
|
minBudget: selectMinBudget || undefined,
|
|
maxBudget: selectMaxBudget || undefined,
|
|
});
|
|
|
|
if (isLoading) return <LoadingPage />;
|
|
if (!data) return <Status500 />;
|
|
|
|
const posizioni = data
|
|
.map((a) => {
|
|
if (a.lat_secondario && a.lon_secondario) {
|
|
return {
|
|
markerTxt: `€ ${a.prezzo / 1e2}`,
|
|
onClick: () => setSelected(a),
|
|
pos: {
|
|
lat: parseFloat(a.lat_secondario),
|
|
lng: parseFloat(a.lon_secondario),
|
|
},
|
|
};
|
|
}
|
|
return undefined;
|
|
})
|
|
.filter((a) => a !== undefined);
|
|
|
|
return (
|
|
<MapComp
|
|
groupData={posizioni}
|
|
height="100%"
|
|
isGroup
|
|
markerType="price"
|
|
onMapClick={() => setSelected(null)}
|
|
posix={{ pos: { lat: 45.764718, lng: 11.73067 } }}
|
|
scrollWheelZoom
|
|
width="100%"
|
|
zoom={13}
|
|
/>
|
|
);
|
|
},
|
|
);
|
|
|
|
MapDisplay.displayName = "MapDisplay";
|
|
|
|
const SelectedComp = memo(
|
|
({ selected }: { selected: AnnuncioRicercaWPosition | null }) => {
|
|
const { t } = useTranslation();
|
|
|
|
return (
|
|
<Fragment>
|
|
<AnimatePresence initial={false}>
|
|
{selected && (
|
|
<motion.a
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
aria-label="Visualizza Dettagli Annuncio"
|
|
className="group absolute right-0 bottom-0 left-0 z-20 mx-2 mb-2 flex flex-col gap-2 rounded-md bg-primary-foreground p-4 text-foreground ring-neutral-500 hover:ring-1"
|
|
exit={{ opacity: 0, scale: 0 }}
|
|
href={`/annuncio/${selected.codice}`}
|
|
initial={{ opacity: 0, scale: 0 }}
|
|
key={selected.id}
|
|
rel="noopener noreferrer"
|
|
target="_blank"
|
|
transition={{
|
|
duration: 0.1,
|
|
scale: { bounce: 0.2, type: "spring", visualDuration: 0.1 },
|
|
}}
|
|
>
|
|
<div className="flex gap-2">
|
|
{selected.images?.[0] ? (
|
|
<Image
|
|
alt="a"
|
|
className="size-24 rounded-md object-cover sm:size-40"
|
|
height={500}
|
|
src={getStorageUrl({
|
|
storageId: selected.images[0].img,
|
|
params: {
|
|
media: "image",
|
|
cacheKey: selected.media_updated_at?.toISOString(),
|
|
},
|
|
})}
|
|
width={500}
|
|
/>
|
|
) : (
|
|
<div className="size-20 bg-gray-200 sm:size-40" />
|
|
)}
|
|
<div className="flex flex-col sm:gap-2 sm:text-lg">
|
|
<h6 className="font-semibold underline-offset-2 group-hover:underline">
|
|
{selected.codice}
|
|
</h6>
|
|
<h3 className="text-sm sm:text-base">{selected.titolo_it}</h3>
|
|
<div className="flex gap-2">
|
|
<h3 className="font-semibold">
|
|
{formatCurrency(selected.prezzo / 1e2)}
|
|
</h3>
|
|
<h3>{selected.mq}mq</h3>
|
|
</div>
|
|
<p>
|
|
{handleConsegna({
|
|
aggiornamento: t.card.in_aggiornamento,
|
|
consegna: selected.consegna,
|
|
consegna_da: t.card.consegna_da,
|
|
mesi: t.parametri.mesi,
|
|
subito: t.card.consegna_subito,
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</motion.a>
|
|
)}
|
|
</AnimatePresence>
|
|
</Fragment>
|
|
);
|
|
},
|
|
);
|
|
SelectedComp.displayName = "SelectedComp";
|