infoalloggi-monorepo/apps/infoalloggi/src/server/controllers/annunci.controller.ts
Marco Pedone 8f629c272a Add Open Graph meta tags for improved SEO and social sharing
Implement API route for fetching images with caching support
Fix cover image URL handling in getAnnunciByCod function
2025-08-08 10:27:21 +02:00

475 lines
12 KiB
TypeScript

import type { Annunci, AnnunciId } from "~/schemas/public/Annunci";
import { TRPCError } from "@trpc/server";
import { createSrc, createSrcset } from "~/server/services/imageServer";
import { AnnuncioObjectWithImages } from "~/server/services/annunci.service";
import { zAnnuncioId } from "~/server/utils/zod_types";
import { z } from "zod/v4";
import { db } from "~/server/db";
import type { ServizioServizioId } from "~/schemas/public/Servizio";
// const ratelimit = new RateLimiterHandler({
// windowSize: 10,
// maxRequests: 10,
// analytics: true,
// });
export const getAnnunciListHandler = async (): Promise<
Pick<
Annunci,
"id" | "codice" | "titolo_it" | "comune" | "tipo" | "locatore" | "stato"
>[]
> => {
try {
return await db
.selectFrom("annunci")
.select([
"id",
"codice",
"titolo_it",
"comune",
"tipo",
"locatore",
"stato",
])
.execute();
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore query getAllRaw",
});
}
};
export const getCodici_AnnunciHandler = async (): Promise<string[]> => {
try {
const codici = await db
.selectFrom("annunci")
.select("codice")
.where("annunci.web", "=", true)
.where("stato", "!=", "Sospeso")
.execute();
if (!codici) {
return [];
}
return codici.map((c) => c.codice);
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore query getCodici",
});
}
};
export type AnnunciWithCoverImage = Annunci & { coverImage: string | null };
export const getAnnunciByCod = async ({
cod,
}: {
cod: string;
}): Promise<AnnunciWithCoverImage | null> => {
try {
const annuncio = await db
.selectFrom("annunci")
.selectAll()
.where("annunci.web", "=", true)
.where("codice", "=", cod)
.executeTakeFirst();
if (!annuncio) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Annuncio non trovato",
});
}
const coverImage = annuncio.url_immagini
? annuncio.url_immagini[0]
? annuncio.url_immagini[0]
: null
: null;
const item = { ...annuncio, coverImage: coverImage };
return AnnuncioObjectWithImages<typeof item>(item);
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore query getAnnuncio",
});
}
};
export const getAnnunciById = async ({
id,
}: {
id: AnnunciId;
}): Promise<AnnunciWithCoverImage | null> => {
try {
const annuncio = await db
.selectFrom("annunci")
.selectAll()
.where("id", "=", id)
.executeTakeFirst();
if (!annuncio) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Annuncio non trovato",
});
}
const coverImage = annuncio.url_immagini
? annuncio.url_immagini[0]
? createSrc(annuncio.url_immagini[0])
: null
: null;
const item = { ...annuncio, coverImage: coverImage };
return AnnuncioObjectWithImages<typeof item>(item);
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore query getAnnuncioById",
});
}
};
export type AnnuncioRicercaWPosition = AnnuncioRicerca & {
lat_secondario: string | null;
lon_secondario: string | null;
};
export const get_AnnunciPositionsHandler = async ({
tipologia,
comune,
consegna,
}: {
tipologia?: string;
comune?: string;
consegna?: number;
}): Promise<AnnuncioRicercaWPosition[]> => {
try {
let query = db
.selectFrom("annunci")
.select([
"id",
"codice",
"comune",
"provincia",
"prezzo",
"consegna",
"numero_camere",
"mq",
"tipo",
"titolo_it",
"titolo_en",
"url_immagini",
"modificato_il",
"stato",
"lon_secondario",
"lat_secondario",
])
.where("web", "=", true)
.where("stato", "!=", "Sospeso")
.orderBy("id", "asc");
if (tipologia && tipologia !== "") {
query = query.where("tipo", "=", tipologia);
}
if (comune && comune !== "") {
query = query.where("comune", "like", comune);
}
if (consegna) {
query = query.where("consegna", "=", consegna);
}
const annunci = await query.execute();
if (!annunci) {
return [];
}
for (const annuncio of annunci) {
annuncio.url_immagini = annuncio.url_immagini
? createSrcset(annuncio.url_immagini)
: [];
}
return annunci;
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore query getAnnunciPositions: " + (e as Error).message,
});
}
};
export type AnnuncioRicerca = Pick<
Annunci,
| "id"
| "codice"
| "comune"
| "provincia"
| "prezzo"
| "consegna"
| "numero_camere"
| "mq"
| "tipo"
| "titolo_it"
| "titolo_en"
| "url_immagini"
| "modificato_il"
| "stato"
>;
export const getCursor_AnnunciHandler = async ({
input,
}: {
input: {
limit?: number;
cursor?: string;
tipologia?: string;
comune?: string;
consegna?: number;
sort?: "Recenti" | "Prezzo" | "Consegna" | "Mq";
};
}): Promise<{
annunci: AnnuncioRicerca[];
nextCursor: string | undefined;
}> => {
const limit = input.limit ?? 50;
const [cursor, sortCursor] = input.cursor?.split("_") ?? [0, undefined];
function sortType(sort: "Recenti" | "Prezzo" | "Consegna" | "Mq") {
switch (sort) {
case "Recenti":
return { col: "modificato_il", opr: "desc" };
case "Prezzo":
return { col: "prezzo", opr: "asc" };
case "Consegna":
return { col: "consegna", opr: "asc" };
case "Mq":
return { col: "mq", opr: "asc" };
}
}
try {
let query = db
.selectFrom("annunci")
.select([
"id",
"codice",
"comune",
"provincia",
"prezzo",
"consegna",
"numero_camere",
"mq",
"tipo",
"titolo_it",
"titolo_en",
"url_immagini",
"modificato_il",
"stato",
])
.where("web", "=", true)
.where("stato", "!=", "Sospeso");
if (sortCursor && input.sort !== undefined) {
const { col: sortColumn, opr } = sortType(input.sort) as {
col: keyof Annunci;
opr: string;
};
query = query.where((eb) =>
eb.or([
eb.and([
eb("id", ">=", (cursor as AnnunciId) ?? 0),
eb(sortColumn, "=", sortCursor.toString()),
]),
eb(sortColumn, opr === "desc" ? "<" : ">", sortCursor.toString()),
]),
);
} else {
query = query.where("id", ">=", (cursor as AnnunciId) ?? 0);
}
if (input.tipologia && input.tipologia !== "") {
query = query.where("tipo", "=", input.tipologia);
}
if (input.comune && input.comune !== "") {
query = query.where("comune", "like", input.comune);
}
if (input.consegna) {
query = query.where("consegna", "=", input.consegna);
}
if (input.sort) {
switch (input.sort) {
case "Recenti":
query = query.orderBy("modificato_il", "desc");
break;
case "Prezzo":
query = query.orderBy("prezzo", "asc");
break;
case "Consegna":
query = query.orderBy("consegna", "asc");
break;
case "Mq":
query = query.orderBy("mq", "asc");
break;
}
}
query = query.orderBy("id", "asc");
const itemsraw = await query.limit(limit + 1).execute();
const annunci = itemsraw;
let nextCursor: string | undefined = undefined;
if (annunci.length > limit) {
const nextItem = annunci.pop(); // return the last item from the array
if (!nextItem) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore interno",
});
}
if (!input.sort) {
nextCursor = nextItem.id.toString();
} else {
const { col: sortColumn } = sortType(input.sort);
if (sortColumn === "modificato_il") {
if (!nextItem.modificato_il) {
nextCursor = `${nextItem.id.toString()}_1990-01-01`;
} else {
nextCursor = `${nextItem.id.toString()}_${nextItem.modificato_il.toISOString()}`;
}
} else {
nextCursor = `${nextItem.id.toString()}_${nextItem[sortColumn as keyof typeof nextItem]?.toString()}`;
}
}
}
for (const annuncio of annunci) {
annuncio.url_immagini = annuncio.url_immagini
? createSrcset(annuncio.url_immagini)
: [];
}
return {
annunci,
nextCursor,
};
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore query getWithCursor",
});
}
};
export const getOptions_AnnunciHandler = async () => {
try {
const comuni = await db
.selectFrom("annunci")
.select(["comune", "tipo", "consegna"])
.where("web", "=", true)
.where("stato", "!=", "Sospeso")
.distinct()
.execute();
const results: { comune: string; tipo: string; consegna: number }[] = [];
for (const comune of comuni) {
if (comune.comune && comune.tipo && comune.consegna) {
results.push({
comune: comune.comune,
tipo: comune.tipo,
consegna: comune.consegna,
});
}
}
return results;
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore query getComuni",
});
}
};
export const EdiAnnuncioSchema = z.object({
annuncioId: zAnnuncioId,
titolo_it: z.string().nonempty("Inserisci un titolo valido"),
desc_it: z.string().nonempty("Inserisci una descrizione valida"),
titolo_en: z.string().nonempty("Inserisci un titolo valido"),
desc_en: z.string().nonempty("Inserisci una descrizione valida"),
status: z.string().nonempty("Seleziona uno stato"),
});
type EditAnnuncioFormValues = z.infer<typeof EdiAnnuncioSchema>;
export const editAnnuncioHandler = async ({
annuncioId,
titolo_it,
desc_it,
titolo_en,
desc_en,
status,
}: EditAnnuncioFormValues) => {
try {
await db
.updateTable("annunci")
.set({
titolo_it: titolo_it,
desc_it: desc_it,
titolo_en: titolo_en,
desc_en: desc_en,
stato: status,
})
.where("id", "=", annuncioId)
.execute();
return true;
} catch {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore interno",
});
}
};
export const getProprietarioDataHandler = async ({
annuncioId,
servizioId,
}: {
annuncioId: AnnunciId;
servizioId: ServizioServizioId;
}) => {
try {
const opened_at = await db
.selectFrom("servizio_annunci")
.select("open_contatti_at")
.where("servizio_id", "=", servizioId)
.where("annunci_id", "=", annuncioId)
.executeTakeFirst();
if (!opened_at || !opened_at.open_contatti_at) {
return { propData: null, opened_at: null };
}
const propData = await db
.selectFrom("annunci")
.select([
"codice",
"comune",
"indirizzo",
"civico",
"provincia",
"regione",
"cap",
"lat",
"lon",
"numero",
"locatore",
])
.where("id", "=", annuncioId)
.executeTakeFirstOrThrow();
return { propData, opened_at: opened_at.open_contatti_at };
} catch (e) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "Errore recupero dati proprietario: " + (e as Error).message,
});
}
};