2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Armchair ,
2026-01-20 15:30:25 +01:00
ArrowUp01 ,
2025-12-11 15:53:47 +01:00
BadgeEuro ,
2025-08-28 18:27:07 +02:00
Bath ,
BedDouble ,
Car ,
Clock ,
Copy ,
ExternalLink ,
Ruler ,
Share2 ,
2025-11-14 17:21:21 +01:00
TrafficCone ,
2025-08-04 17:45:44 +02:00
} from "lucide-react" ;
import type { GetStaticPaths , GetStaticPropsContext , NextPage } from "next" ;
import Head from "next/head" ;
import Link from "next/link" ;
2025-08-28 18:27:07 +02:00
import { useEffect , useState } from "react" ;
import toast from "react-hot-toast" ;
import { CarouselAnnuncio } from "~/components/annuncio_card" ;
import { AnnuncioInteractions } from "~/components/annuncio-interactions/annuncio_interactions" ;
import { TouchProvider } from "~/components/custom_ui/HybridTooltip" ;
2026-01-12 16:00:53 +01:00
import { ComeFunziona } from "~/components/expand_guida" ;
2025-08-28 18:27:07 +02:00
import FailedAnnuncioLoading from "~/components/failed-loading" ;
2026-03-17 18:47:30 +01:00
import { IconMatrix } from "~/components/IconComponents" ;
2025-08-28 18:27:07 +02:00
import InformationBubble from "~/components/InformationBubble" ;
2025-08-22 16:27:58 +02:00
import { LoadingPage } from "~/components/loading" ;
2025-08-04 17:45:44 +02:00
import { MappaDialogFullscreen } from "~/components/MapDialog" ;
2026-03-20 14:32:46 +01:00
import { PricingComponent , type PricingData } from "~/components/prezzi" ;
2025-08-28 18:27:07 +02:00
import {
Accordion ,
AccordionContent ,
AccordionItem ,
AccordionTrigger ,
} from "~/components/ui/accordion" ;
2025-09-01 11:55:45 +02:00
import { Button } from "~/components/ui/button" ;
2025-08-04 17:45:44 +02:00
import { Card , CardContent , CardHeader , CardTitle } from "~/components/ui/card" ;
2026-01-12 17:08:27 +01:00
import {
Collapsible ,
CollapsibleContent ,
CollapsibleTrigger ,
} from "~/components/ui/collapsible" ;
2025-08-04 17:45:44 +02:00
import {
2025-08-28 18:27:07 +02:00
Dialog ,
DialogContent ,
DialogDescription ,
DialogHeader ,
DialogTitle ,
DialogTrigger ,
2025-08-04 17:45:44 +02:00
} from "~/components/ui/dialog" ;
2026-03-18 17:44:42 +01:00
import Input from "~/components/ui/input" ;
2025-08-04 17:45:44 +02:00
import { Label } from "~/components/ui/label" ;
import { VideoPlayer } from "~/components/videoPlayer" ;
2025-10-20 16:22:20 +02:00
import { env } from "~/env" ;
2026-03-17 18:47:30 +01:00
import { filteredCaratteristiche } from "~/hooks/schedaAnnuncioUtils" ;
2025-12-05 16:36:17 +01:00
import { useStaleImageReload } from "~/hooks/staleImage" ;
2025-08-28 18:27:07 +02:00
import { useMediaQuery } from "~/hooks/use-media-query" ;
import { handleConsegna } from "~/lib/annuncio_details" ;
2025-08-29 18:18:37 +02:00
import { replaceWithBr } from "~/lib/newlineToBr" ;
2025-11-28 15:11:14 +01:00
import { getStorageUrl } from "~/lib/storage_utils" ;
2025-08-28 18:27:07 +02:00
import { cn } from "~/lib/utils" ;
2026-03-16 17:56:22 +01:00
import { AnnuncioContext , useAnnuncio } from "~/providers/AnnuncioProvider" ;
2025-08-28 18:27:07 +02:00
import { useTranslation } from "~/providers/I18nProvider" ;
2026-01-20 18:38:17 +01:00
import { buildRicercaUrl } from "~/providers/RicercaProvider" ;
2025-08-28 18:27:07 +02:00
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum" ;
2026-03-19 10:20:41 +01:00
import type { AnnuncioData } from "~/server/controllers/annunci.controller" ;
2025-08-28 18:27:07 +02:00
import { generateSSGHelper } from "~/server/utils/ssgHelper" ;
import { api } from "~/utils/api" ;
2025-08-04 17:45:44 +02:00
type AnnuncioProps = {
2026-03-19 10:20:41 +01:00
data : AnnuncioData ;
2025-08-28 18:27:07 +02:00
flag : string ;
2025-08-04 17:45:44 +02:00
} ;
2026-03-09 17:33:02 +01:00
/ * *
* Pagina di dettaglio annuncio : /annuncio/ [ cod ]
* Questa pagina mostra i dettagli di un annuncio specifico , identificato dal codice ( cod ) passato come parametro . Utilizza getStaticProps per prefetchare i dati dell 'annuncio e le relative informazioni meta per SEO e condivisione sui social media. Se l' annuncio è in stato "Sospeso" o se si verifica un errore durante il caricamento , viene mostrata una pagina di errore .
* /
2025-08-04 17:45:44 +02:00
const AnnuncioDettaglio : NextPage < AnnuncioProps > = ( {
2026-03-19 10:20:41 +01:00
data ,
2025-08-28 18:27:07 +02:00
flag ,
2025-08-04 17:45:44 +02:00
} : AnnuncioProps ) = > {
2025-08-28 18:27:07 +02:00
return (
< main >
< Head >
2026-03-19 10:20:41 +01:00
< title > { ` ${ data . codice } | ${ data . titolo_it } ` } < / title >
< meta content = { data . desc_it || "" } property = "description" / >
< meta content = { data . ogUrl } property = "og:url" / >
2025-08-29 16:18:32 +02:00
< meta content = "website" property = "og:type" / >
2026-02-17 12:36:44 +01:00
< meta
2026-03-19 10:20:41 +01:00
content = { ` ${ data . codice } | ${ data . titolo_it } ` }
2026-02-17 12:36:44 +01:00
property = "og:title"
/ >
2026-03-19 10:20:41 +01:00
< meta content = { data . desc_it || "" } property = "og:description" / >
< meta content = { data . ogImage } property = "og:image" / >
2025-08-29 16:18:32 +02:00
< meta content = "1200" property = "og:image:width" / >
< meta content = "630" property = "og:image:height" / >
2025-08-28 18:27:07 +02:00
< meta
content = { ` ${ env . NEXT_PUBLIC_BASE_URL } /Infoalloggi.png ` }
2025-08-29 16:18:32 +02:00
property = "og:logo"
2025-08-28 18:27:07 +02:00
/ >
2025-08-29 16:18:32 +02:00
< meta content = "Infoalloggi.it" property = "og:site_name" / >
2026-03-19 10:20:41 +01:00
< link href = { data . ogUrl || "" } rel = "canonical" / >
2026-01-07 12:31:34 +01:00
< link
2026-03-19 10:20:41 +01:00
href = { ` ${ env . NEXT_PUBLIC_BASE_URL } /annuncio/ ${ data . codice } ` }
2026-01-07 12:31:34 +01:00
hrefLang = "it"
rel = "alternate"
/ >
< link
2026-03-19 10:20:41 +01:00
href = { ` ${ env . NEXT_PUBLIC_BASE_URL } /en/annuncio/ ${ data . codice } ` }
2026-01-07 12:31:34 +01:00
hrefLang = "en"
rel = "alternate"
/ >
2025-08-28 18:27:07 +02:00
< / Head >
2026-03-19 10:20:41 +01:00
< AnnuncioView data = { data } flag = { flag } / >
2025-08-28 18:27:07 +02:00
< / main >
) ;
2025-08-08 14:13:27 +02:00
} ;
2026-03-19 10:20:41 +01:00
const AnnuncioView = ( { data , flag } : AnnuncioProps ) = > {
2025-12-05 16:36:17 +01:00
useStaleImageReload ( ) ;
2025-08-28 18:27:07 +02:00
const isDesktop = useMediaQuery ( "(min-width: 768px)" ) ;
2026-04-10 10:00:34 +02:00
if ( ! data || ( data && data . stato === "Sospeso" ) ) {
2025-08-28 18:27:07 +02:00
return < FailedAnnuncioLoading / > ;
}
return (
< AnnuncioContext.Provider value = { data } >
< TouchProvider >
2026-01-16 10:30:27 +01:00
< div className = "mx-auto w-full max-w-400 px-2 sm:px-8" >
2025-09-01 15:59:08 +02:00
< CarouselAnnuncio
2025-11-18 15:20:14 +01:00
immagini = { data . images }
2026-03-19 10:20:41 +01:00
updated_at = {
data . media_updated_at ? new Date ( data . media_updated_at ) : null
}
2025-11-18 15:20:14 +01:00
videos = { data . videos }
2025-09-01 15:59:08 +02:00
/ >
2025-08-28 18:27:07 +02:00
< div className = "flex flex-col sm:py-8 md:flex-row-reverse md:gap-2" >
< div className = "mb-6 flex flex-col md:mb-0 md:w-1/3" >
< div className = "flex w-full flex-col flex-nowrap items-center justify-center gap-6" >
2026-03-17 18:47:30 +01:00
< CardInfos / >
2026-04-16 16:31:25 +02:00
< AnnuncioInteractions disabled = { flag === "true" } / >
2026-01-12 16:00:53 +01:00
< ComeFunziona / >
2025-08-28 18:27:07 +02:00
{ data . tipo &&
( data . tipo === "Transitorio" || data . tipo === "Stabile" ) && (
< >
2026-03-16 17:56:22 +01:00
< Pricing isDesktop = { isDesktop } / >
2025-08-28 18:27:07 +02:00
< / >
) }
< / div >
< / div >
< div className = "mx-2 md:w-2/3" >
2026-03-17 18:47:30 +01:00
< TestiAnnuncio / >
2025-08-28 18:27:07 +02:00
< / div >
< / div >
2026-03-17 18:47:30 +01:00
< AnnuncioFooter / >
2025-08-28 18:27:07 +02:00
< / div >
< / TouchProvider >
< / AnnuncioContext.Provider >
) ;
2025-08-04 17:45:44 +02:00
} ;
export async function getStaticProps (
2025-08-28 18:27:07 +02:00
context : GetStaticPropsContext < { cod : string } > ,
2025-08-04 17:45:44 +02:00
) {
2025-08-28 18:27:07 +02:00
const ssg = generateSSGHelper ( ) ;
const cod = context . params ? . cod ;
if ( typeof cod !== "string" ) {
return {
2026-03-19 10:20:41 +01:00
redirect : {
destination : "/annuncio-non-trovato" ,
permanent : false ,
} ,
} ;
}
const annuncio = await ssg . annunci . getAnnuncioData . fetch ( { cod : cod } ) ;
if ( ! annuncio . success || annuncio . data . stato === "Sospeso" ) {
return {
redirect : {
destination : "/annuncio-non-trovato" ,
permanent : false ,
} ,
2025-08-28 18:27:07 +02:00
} ;
}
2025-11-20 12:38:42 +01:00
const flag = await ssg . flags . GetFlagValue . fetch ( {
2025-08-28 18:27:07 +02:00
id : "ANNUNCIO_INTERACTIONS_DISABLED" ,
} ) ;
return {
props : {
2026-03-19 10:20:41 +01:00
data : annuncio.data ,
//cod,
2025-08-28 18:27:07 +02:00
flag ,
2026-03-19 10:20:41 +01:00
//meta,
2025-08-29 16:18:32 +02:00
trpcState : ssg.dehydrate ( ) ,
2025-08-28 18:27:07 +02:00
} ,
2025-10-17 19:14:27 +02:00
revalidate : 21600 , // 6 hours - ensures fresh data 4x per day
2025-08-28 18:27:07 +02:00
} ;
2025-08-04 17:45:44 +02:00
}
export const getStaticPaths : GetStaticPaths = async ( ) = > {
2025-08-28 18:27:07 +02:00
return {
fallback : "blocking" ,
2025-08-29 16:18:32 +02:00
paths : [ ] ,
2025-08-28 18:27:07 +02:00
} ;
2025-08-04 17:45:44 +02:00
} ;
export default AnnuncioDettaglio ;
2026-03-16 17:56:22 +01:00
const Pricing = ( { isDesktop } : { isDesktop : boolean } ) = > {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation ( ) ;
2026-03-16 17:56:22 +01:00
const { prezzo , tipo } = useAnnuncio ( ) ;
const tipologia =
tipo === "Transitorio"
? TipologiaPosizioneEnum . Transitorio
: TipologiaPosizioneEnum . Stabile ;
2025-08-28 18:27:07 +02:00
const { data : prezziario , isLoading } =
api . prezziario . getPrezziPerTipologia . useQuery ( {
tipologia ,
} ) ;
if ( isLoading ) return < LoadingPage / > ;
if ( ! prezziario ) return null ;
2026-03-20 14:32:46 +01:00
const opzioniT = [
t . pricing_cmp . breve_option1 ,
t . pricing_cmp . breve_option2 ,
t . pricing_cmp . breve_option3 ,
t . pricing_cmp . breve_option4 ,
t . pricing_cmp . breve_option5 ,
] ;
const opzioniS = [
t . pricing_cmp . stabile_option1 ,
t . pricing_cmp . stabile_option2 ,
t . pricing_cmp . stabile_option3 ,
] ;
2025-08-28 18:27:07 +02:00
const cta =
tipologia === TipologiaPosizioneEnum . Transitorio
? t . pricing_cmp . cta_breve
: t . pricing_cmp . cta_stabile ;
const pricing = prezziario . saldi . map ( ( s ) = > ( {
downPayment : prezziario.acconto.prezzo_cent / 100 ,
secondPayment : s.prezzo_cent / 100 ,
} ) ) ;
2026-03-20 14:32:46 +01:00
let opzioni : string [ ] = [ ] ;
let pricingData : PricingData = [ ] ;
if ( tipologia === TipologiaPosizioneEnum . Stabile ) {
//se stabile, mostra il prezzo basato sul canone in default
const idx = prezzo > 70000 ? 2 : prezzo > 50000 ? 1 : 0 ;
const opzione = opzioniS [ idx ] ;
const pricingS = pricing [ idx ] ;
if ( opzione && pricingS ) {
opzioni = [ opzione ] ;
pricingData = [ pricingS ] ;
} else {
opzioni = opzioniS ;
pricingData = pricing ;
}
} else {
opzioni = opzioniT ;
pricingData = pricing ;
}
const style =
tipologia === TipologiaPosizioneEnum . Transitorio
? "border-transitorio bg-transitorio"
: "border-stabile bg-stabile" ;
const title =
tipologia === TipologiaPosizioneEnum . Transitorio
? t . prezzi . titolo_transitori
: t . prezzi . titolo_stabile ;
2026-03-16 17:56:22 +01:00
2025-08-28 18:27:07 +02:00
if ( isDesktop ) {
return (
< PricingComponent
className = { cn (
style ,
2026-01-16 10:30:27 +01:00
"max-w-2xl **:data-[role=wrapper]:flex-wrap **:data-[role=amount]:text-2xl lg:**:data-[role=wrapper]:flex-nowrap xl:**:data-[role=amount]:text-3xl sm:[&_#contratto-link]:h-8 xl:[&_#contratto-link]:h-9" ,
2025-08-28 18:27:07 +02:00
) }
2025-08-29 16:18:32 +02:00
cta = { cta }
opzioni = { opzioni }
2026-03-20 14:32:46 +01:00
pricingData = { pricingData }
2025-08-29 16:18:32 +02:00
statico = { true }
tipo = { tipologia }
2026-03-20 14:32:46 +01:00
title = { title }
2025-08-28 18:27:07 +02:00
/ >
) ;
}
2025-09-01 16:48:03 +02:00
return (
2026-01-12 17:08:27 +01:00
< Collapsible className = "w-full" >
< CollapsibleTrigger asChild >
< Button
className = { cn (
"w-full text-white" ,
tipologia === TipologiaPosizioneEnum . Transitorio
? "bg-transitorio"
: "bg-stabile" ,
) }
2026-03-09 16:00:54 +01:00
data - umami - event = { ` pricing_mobile_cta_click ` }
2026-01-12 17:08:27 +01:00
>
< div className = "flex items-center gap-2" >
< BadgeEuro className = "size-5" / >
2025-12-11 15:53:47 +01:00
< span > { t . pricing_cmp . cta_costo } < / span >
< / div >
2026-01-12 17:08:27 +01:00
< / Button >
< / CollapsibleTrigger >
< CollapsibleContent className = "mt-2" >
< PricingComponent
2026-01-20 15:30:25 +01:00
className = { cn ( style , "max-w-full" ) }
2026-01-12 17:08:27 +01:00
cta = { cta }
opzioni = { opzioni }
2026-03-20 14:32:46 +01:00
pricingData = { pricingData }
2026-01-12 17:08:27 +01:00
statico = { true }
tipo = { tipologia }
2026-03-20 14:32:46 +01:00
title = { title }
2026-01-12 17:08:27 +01:00
/ >
< / CollapsibleContent >
< / Collapsible >
2025-09-01 16:48:03 +02:00
) ;
2025-08-04 17:45:44 +02:00
} ;
2026-03-17 18:47:30 +01:00
const TestiAnnuncio = ( ) = > {
const { locale } = useTranslation ( ) ;
const { titolo_it , titolo_en , desc_it , desc_en } = useAnnuncio ( ) ;
2025-08-28 18:27:07 +02:00
return (
< >
2026-03-17 18:47:30 +01:00
< h1 className = "font-bold text-3xl uppercase" >
{ locale === "it" ? titolo_it : titolo_en }
< / h1 >
2025-08-28 18:27:07 +02:00
< h1
className = "py-6"
dangerouslySetInnerHTML = { {
2026-03-17 18:47:30 +01:00
__html : replaceWithBr ( ( locale === "it" ? desc_it : desc_en ) || "" ) ,
2025-08-28 18:27:07 +02:00
} }
/ >
2026-03-17 18:47:30 +01:00
< CaratteristicheAccordion / >
2025-08-28 18:27:07 +02:00
< / >
) ;
2025-08-04 17:45:44 +02:00
} ;
2026-03-17 18:47:30 +01:00
const CaratteristicheAccordion = ( ) = > {
const { caratteristiche } = useAnnuncio ( ) ;
if ( ! caratteristiche ) return null ;
2025-08-28 18:27:07 +02:00
return (
< Accordion
className = "my-5"
collapsible
defaultValue = "altre-caratt"
2025-08-29 16:18:32 +02:00
type = "single"
2025-08-28 18:27:07 +02:00
>
2026-02-24 11:36:26 +01:00
< AccordionItem
className = "border-b dark:border-muted-foreground/50"
value = "altre-caratt"
>
2025-08-28 18:27:07 +02:00
< AccordionTrigger > Altre caratteristiche < / AccordionTrigger >
< AccordionContent >
2026-03-17 18:47:30 +01:00
{ Object . entries ( filteredCaratteristiche ( caratteristiche ) ) . map (
( [ key , scheda_value ] ) = > {
const { text , icon , value } = scheda_value ;
if ( value === null ) return null ;
return (
< div
className = "flex flex-row items-center gap-2 text-base"
key = { key }
>
< IconMatrix className = "size-4" type = { icon } / >
{ text } : { value }
< / div >
) ;
} ,
) }
2025-08-28 18:27:07 +02:00
< / AccordionContent >
< / AccordionItem >
< / Accordion >
) ;
2025-08-04 17:45:44 +02:00
} ;
2026-03-17 18:47:30 +01:00
const CardInfos = ( ) = > {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation ( ) ;
2026-03-17 18:47:30 +01:00
const {
codice ,
prezzo ,
tipo ,
numero_camere ,
numero_bagni ,
caratteristiche ,
numero_postiauto ,
mq ,
piano ,
stato ,
consegna ,
lat_secondario ,
lon_secondario ,
} = useAnnuncio ( ) ;
2025-08-28 18:27:07 +02:00
return (
2025-11-14 17:21:21 +01:00
< Card className = "relative w-full bg-secondary text-secondary-foreground outline outline-secondary-foreground" >
2025-08-28 18:27:07 +02:00
< CardHeader >
2025-10-10 16:18:43 +02:00
< CardTitle className = "text-center font-bold text-3xl md:pl-8 lg:pl-0" >
2026-03-17 18:47:30 +01:00
{ t . annunci . codice } { codice }
2025-08-28 18:27:07 +02:00
< / CardTitle >
< / CardHeader >
< CardContent className = "flex flex-col gap-4 p-4 py-0" >
< div className = "grid grid-cols-2 gap-3 font-semibold" >
< div className = "relative" >
2026-03-02 15:35:23 +01:00
< div className = "flex h-10 items-center justify-center rounded-md bg-primary-foreground text-center text-primary" >
2026-03-17 18:47:30 +01:00
{ prezzo && ( prezzo / 1 e2 ) . toFixed ( 2 ) . replace ( "." , "," ) } { " " }
2026-01-20 15:30:25 +01:00
{ t . annunci . euro_mese }
2025-08-28 18:27:07 +02:00
< / div >
2026-03-17 18:47:30 +01:00
{ prezzo && (
2025-08-28 18:27:07 +02:00
< InformationBubble
icon = "euro"
iconClassName = "h-[19.5px] w-[19.5px] bg-orange-500 text-white rounded-full"
>
2025-11-14 17:21:21 +01:00
< div className = "text-fxd-foreground" >
< span className = "font-semibold text-sm" >
2026-01-20 15:30:25 +01:00
{ t . annunci . prezzo_giorno }
2025-08-28 18:27:07 +02:00
< / span >
2025-11-14 17:21:21 +01:00
< div className = "text-sm" >
2026-03-17 18:47:30 +01:00
{ ( prezzo / 1 e2 / 30 ) . toFixed ( 2 ) . replace ( "." , "," ) } { " " }
2026-01-20 15:30:25 +01:00
{ t . annunci . euro_giorno }
2025-08-28 18:27:07 +02:00
< / div >
< / div >
< / InformationBubble >
) }
< / div >
< div className = "relative" >
< div
className = { cn (
2026-03-02 15:35:23 +01:00
"h-10 rounded-md bg-primary-foreground p-2 text-center text-primary outline-2" ,
2026-03-17 18:47:30 +01:00
tipo === "Transitorio" && "outline-transitorio" ,
tipo === "Stabile" && "outline-stabile" ,
2025-08-28 18:27:07 +02:00
) }
>
2026-03-17 18:47:30 +01:00
{ tipo }
2025-08-28 18:27:07 +02:00
< / div >
2026-03-17 18:47:30 +01:00
{ tipo === "Transitorio" && (
2025-08-28 18:27:07 +02:00
< InformationBubble
href = "/guida#faq-transitori"
target = "_blank"
txt = "Più informazioni sugli affitti transitori"
/ >
) }
< / div >
< / div >
< div className = "flex flex-col gap-2" >
2026-01-20 15:30:25 +01:00
< h3 className = "px-2" > { t . annunci . fornitura } : < / h3 >
2025-08-28 18:27:07 +02:00
< div className = "grid grid-cols-2 gap-3" >
2025-11-14 17:21:21 +01:00
< div className = "flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary" >
2025-08-28 18:27:07 +02:00
< BedDouble / >
2026-03-02 15:35:23 +01:00
< span className = "line-clamp-1 break-all" >
{ ( ( ) = > {
2026-03-17 18:47:30 +01:00
if ( ! numero_camere ) return "N/A" ;
if ( numero_camere > 1 )
return ` ${ numero_camere } ${ t . card . camere2 } ` ;
if ( numero_camere === 1 )
return ` ${ numero_camere } ${ t . card . camere1 } ` ;
2026-03-02 15:35:23 +01:00
} ) ( ) }
< / span >
2025-08-28 18:27:07 +02:00
< / div >
2025-11-14 17:21:21 +01:00
< div className = "flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary" >
2025-08-28 18:27:07 +02:00
< Bath / >
2026-03-02 15:35:23 +01:00
< span className = "line-clamp-1 break-all" >
2026-03-17 18:47:30 +01:00
{ numero_bagni || 0 } { t . annunci . bagni }
2026-01-20 15:30:25 +01:00
< / span >
2025-08-28 18:27:07 +02:00
< / div >
2025-11-14 17:21:21 +01:00
< div className = "flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary" >
2025-08-28 18:27:07 +02:00
< Armchair / >
2026-03-02 15:35:23 +01:00
< span className = "line-clamp-1 break-all" >
2026-03-17 18:47:30 +01:00
{ caratteristiche ? . Scheda_Arredi || "" }
2026-03-02 15:35:23 +01:00
< / span >
2025-08-28 18:27:07 +02:00
< / div >
2025-11-14 17:21:21 +01:00
< div className = "flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary" >
2025-08-28 18:27:07 +02:00
< Car / >
2026-03-02 15:35:23 +01:00
< span className = "line-clamp-1 break-all" >
2026-03-17 18:47:30 +01:00
{ numero_postiauto || 0 } { t . annunci . posti_auto }
2026-01-20 15:30:25 +01:00
< / span >
2025-08-28 18:27:07 +02:00
< / div >
< / div >
< / div >
< div className = "flex flex-col gap-2" >
2026-01-20 15:30:25 +01:00
< h3 className = "px-2" > { t . annunci . dati_immobile } : < / h3 >
2025-08-28 18:27:07 +02:00
< div className = "grid grid-cols-2 gap-3" >
2025-11-14 17:21:21 +01:00
< div className = "flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary" >
2025-08-28 18:27:07 +02:00
< Ruler / >
2026-03-02 15:35:23 +01:00
< span className = "line-clamp-1 break-all" >
2026-03-17 18:47:30 +01:00
{ mq ? Number ( mq ) . toLocaleString ( "it-IT" ) : "--" } m < sup > 2 < / sup >
2025-08-28 18:27:07 +02:00
< / span >
< / div >
2025-11-14 17:21:21 +01:00
< div className = "flex justify-center gap-2 rounded-md bg-primary-foreground p-2 text-primary" >
2026-03-17 18:47:30 +01:00
{ piano !== null && piano !== "" && (
2026-01-20 15:30:25 +01:00
< >
< ArrowUp01 / >
2026-03-02 15:35:23 +01:00
< span className = "line-clamp-1 break-all" >
2026-01-20 15:30:25 +01:00
{ ( ( ) = > {
2026-03-17 18:47:30 +01:00
if ( piano === "0" ) return t . annunci . p_terra ;
if ( piano === "0.5" ) return t . annunci . p_rialzato ;
if ( piano ) return ` ${ piano } ° ${ t . annunci . piano } ` ;
2026-01-20 15:30:25 +01:00
return "--" ;
} ) ( ) }
< / span >
< / >
) }
2025-08-28 18:27:07 +02:00
< / div >
< / div >
< / div >
2025-11-14 17:21:21 +01:00
< div className = "text-center font-bold text-base" >
2026-03-17 18:47:30 +01:00
{ stato === "Trattativa" ? (
2025-11-14 17:21:21 +01:00
< div className = "flex items-center justify-center gap-1 rounded-md py-1.5 text-trattativa outline-2 outline-trattativa dark:bg-secondary-foreground" >
< TrafficCone className = "inline-block size-5" / >
2026-01-20 15:30:25 +01:00
< span > { t . annunci . in_aggiornamento } < / span >
2025-11-14 17:21:21 +01:00
< / div >
2025-08-28 18:27:07 +02:00
) : (
2025-11-14 17:21:21 +01:00
< div className = "flex items-center justify-center gap-1 rounded-md py-1.5 outline-2 outline-secondary-foreground" >
2025-08-28 18:27:07 +02:00
< Clock className = "inline-block size-5" / >
2026-01-20 15:30:25 +01:00
{ t . annunci . disponibile } : { " " }
2025-08-28 18:27:07 +02:00
< span className = "text-red-500" >
{ handleConsegna ( {
2025-08-29 16:18:32 +02:00
aggiornamento : t.card.in_aggiornamento ,
2026-03-17 18:47:30 +01:00
consegna : consegna ,
2025-08-28 18:27:07 +02:00
consegna_da : t.card.consegna_da ,
2025-12-18 12:11:08 +01:00
mesi : t.parametri.mesi ,
2025-08-28 18:27:07 +02:00
subito : t.card.consegna_subito ,
} ) }
< / span >
< / div >
) }
< / div >
< div className = "flex flex-row justify-around gap-3" >
2026-03-17 18:47:30 +01:00
< MappaDialogFullscreen lat = { lat_secondario } long = { lon_secondario } / >
2025-08-28 18:27:07 +02:00
< ShareComponent / >
< / div >
< / CardContent >
< / Card >
) ;
2025-08-04 17:45:44 +02:00
} ;
const ShareComponent = ( ) = > {
2025-08-28 18:27:07 +02:00
const { t } = useTranslation ( ) ;
const [ link , setLink ] = useState ( "" ) ;
useEffect ( ( ) = > {
if ( typeof window !== "undefined" ) {
setLink ( window . location . href ) ;
}
} , [ ] ) ;
const handleCopyLink = async ( ) = > {
if ( navigator . clipboard ? . writeText ) {
try {
await navigator . clipboard . writeText ( link ) ;
toast ( t . share . copiato , { icon : "📋" } ) ;
} catch ( err ) {
console . error ( "Failed to copy: " , err ) ;
}
} else {
// Fallback for browsers that do not support navigator.clipboard.writeText
const textArea = document . createElement ( "textarea" ) ;
textArea . value = link ;
document . body . appendChild ( textArea ) ;
textArea . select ( ) ;
try {
document . execCommand ( "copy" ) ;
toast ( t . share . copiato , { icon : "📋" } ) ;
} catch ( err ) {
console . error ( "Failed to copy: " , err ) ;
}
document . body . removeChild ( textArea ) ;
}
} ;
return (
< Dialog >
< DialogTrigger asChild >
2026-03-09 16:00:54 +01:00
< Button data-umami-event = "share_button_click" >
2026-01-16 15:47:38 +01:00
< Share2 / >
2025-08-28 18:27:07 +02:00
< / Button >
< / DialogTrigger >
< DialogContent className = "sm:max-w-md" >
< DialogHeader >
< DialogTitle > { t . share . condividi } < / DialogTitle >
< DialogDescription > { t . share . descrizione } < / DialogDescription >
< / DialogHeader >
< div className = "flex items-center space-x-2" >
< div className = "grid flex-1 gap-2" >
2025-08-29 16:18:32 +02:00
< Label className = "sr-only" htmlFor = "link" >
2025-08-28 18:27:07 +02:00
Link
< / Label >
2025-08-29 16:18:32 +02:00
< Input defaultValue = { link } id = "link" readOnly / >
2025-08-28 18:27:07 +02:00
< / div >
< Button
className = "px-3"
onClick = { async ( ) = > await handleCopyLink ( ) }
2025-08-29 16:18:32 +02:00
size = "sm"
type = "submit"
2025-08-28 18:27:07 +02:00
>
< span className = "sr-only" > Copy < / span >
< Copy / >
< / Button >
< / div >
< / DialogContent >
< / Dialog >
) ;
2025-08-04 17:45:44 +02:00
} ;
2026-03-17 18:47:30 +01:00
const AnnuncioFooter = ( ) = > {
const { tipo , comune , consegna , videos , external_videos , media_updated_at } =
useAnnuncio ( ) ;
2026-01-20 15:30:25 +01:00
const { t } = useTranslation ( ) ;
2025-08-28 18:27:07 +02:00
return (
< div className = "flex flex-col gap-2" >
2026-03-09 10:18:58 +01:00
< p
className = { cn (
"px-2 font-semibold text-lg" ,
2026-03-17 18:47:30 +01:00
videos . length === 0 && external_videos ? . length === 0 && "hidden" ,
2026-03-09 10:18:58 +01:00
) }
>
Video :
< / p >
2025-08-28 18:27:07 +02:00
< div className = "flex flex-col flex-wrap gap-4 sm:flex-row sm:gap-2 sm:px-2" >
2025-11-18 15:20:14 +01:00
{ videos . map ( ( video ) = > {
2025-09-01 16:48:03 +02:00
return (
< VideoPlayer
className = "h-96 max-w-96"
2025-11-28 15:11:14 +01:00
coverImage = { getStorageUrl ( {
storageId : video.thumb ,
2026-03-17 18:47:30 +01:00
params : {
2026-03-19 10:20:41 +01:00
cacheKey : media_updated_at || undefined ,
2026-03-17 18:47:30 +01:00
media : "image" ,
} ,
2025-11-28 15:11:14 +01:00
} ) }
2025-11-18 15:20:14 +01:00
key = { ` videoplayer- ${ video . video } ` } // Cache busting
2025-11-28 15:11:14 +01:00
videoSrc = { getStorageUrl ( {
storageId : video.video ,
2026-03-17 18:47:30 +01:00
params : {
2026-03-19 10:20:41 +01:00
cacheKey : media_updated_at || undefined ,
2026-03-17 18:47:30 +01:00
media : "video" ,
} ,
2025-11-28 15:11:14 +01:00
} ) }
2025-11-18 15:20:14 +01:00
/ >
) ;
} ) }
2026-03-17 18:47:30 +01:00
{ external_videos ? . map ( ( videoUrl ) = > {
2025-11-18 15:20:14 +01:00
const embedUrl = getYouTubeEmbedUrl ( videoUrl ) ;
if ( ! embedUrl ) return null ;
return (
< iframe
allow = "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
className = "h-96 max-w-96 rounded-lg"
key = { ` external-videoplayer- ${ videoUrl } ` }
loading = "lazy"
referrerPolicy = "strict-origin-when-cross-origin"
src = { embedUrl }
title = "YouTube video player"
2025-09-01 16:48:03 +02:00
/ >
) ;
2025-08-28 18:27:07 +02:00
} ) }
< / div >
< Link
2026-01-20 18:38:17 +01:00
href = { buildRicercaUrl ( {
comune : comune || undefined ,
consegna : consegna ? [ consegna ] : undefined ,
tipo :
tipo in [ "Transitorio" , "Stabile" ]
? ( tipo as "Transitorio" | "Stabile" )
: undefined ,
} ) }
2025-08-28 18:27:07 +02:00
target = "_blank"
>
2026-03-09 16:00:54 +01:00
< Button
className = "my-10 w-full text-xl"
data - umami - event = "similar_listings_click"
variant = "secondary"
>
2026-01-20 15:30:25 +01:00
{ t . annunci . find_similar } < ExternalLink className = "size-6" / >
2025-08-28 18:27:07 +02:00
< / Button >
< / Link >
< / div >
) ;
2025-08-04 17:45:44 +02:00
} ;
2025-11-18 15:20:14 +01:00
export const getYouTubeEmbedUrl = ( url : string ) : string | null = > {
try {
const urlObj = new URL ( url ) ;
let videoId : string | null = null ;
// youtube.com/watch?v=VIDEO_ID
if (
urlObj . hostname . includes ( "youtube.com" ) &&
urlObj . pathname === "/watch"
) {
videoId = urlObj . searchParams . get ( "v" ) ;
}
// youtu.be/VIDEO_ID
if ( urlObj . hostname === "youtu.be" ) {
videoId = urlObj . pathname . slice ( 1 ) ;
}
// youtube.com/embed/VIDEO_ID (already embed format)
if (
urlObj . hostname . includes ( "youtube.com" ) &&
urlObj . pathname . startsWith ( "/embed/" )
) {
videoId = urlObj . pathname . split ( "/embed/" ) [ 1 ] ? . split ( "?" ) [ 0 ] || null ;
}
if ( ! videoId ) return null ;
// Use youtube-nocookie.com domain (privacy-enhanced mode)
// Add parameters to reduce ads and suggestions
const params = new URLSearchParams ( {
rel : "0" , // Don't show related videos from other channels
modestbranding : "1" , // Minimal YouTube branding
controls : "1" , // Show player controls
showinfo : "0" , // Don't show video info before playing
fs : "1" , // Allow fullscreen
iv_load_policy : "3" , // Hide video annotations
disablekb : "0" , // Enable keyboard controls
autoplay : "0" , // Don't autoplay
// cc_load_policy: "1", // Show closed captions (optional)
} ) ;
return ` https://www.youtube-nocookie.com/embed/ ${ videoId } ? ${ params . toString ( ) } ` ;
} catch {
return null ;
}
} ;