"use client"; import { useState } from "react"; import type { AnalogLot } from "@/types/trade-in"; /** XSS prevention: only allow http/https/relative src */ function safeImgSrc(url: string | null): string | null { if (!url) return null; if ( url.startsWith("https://") || url.startsWith("http://") || url.startsWith("/") ) { return url; } return null; } function fmtRub(value: number): string { return value.toLocaleString("ru-RU", { style: "currency", currency: "RUB", maximumFractionDigits: 0, }); } function fmtDate(iso: string | null): string { if (!iso) return "—"; return new Date(iso).toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "2-digit", }); } type SortKey = "price_rub" | "price_per_m2" | "days_on_market"; interface Props { rows: AnalogLot[]; emptyLabel?: string; } export function AnalogsTable({ rows, emptyLabel = "Нет аналогов в радиусе 1 км", }: Props) { const [sortKey, setSortKey] = useState("price_rub"); const [sortAsc, setSortAsc] = useState(true); if (rows.length === 0) { return (
{emptyLabel}
); } const sorted = [...rows].sort((a, b) => { const av = a[sortKey]; const bv = b[sortKey]; // null ('неизвестно') всегда в конце, независимо от направления if (av === null) return bv === null ? 0 : 1; if (bv === null) return -1; return sortAsc ? av - bv : bv - av; }); function handleSort(key: SortKey) { if (sortKey === key) { setSortAsc((prev) => !prev); } else { setSortKey(key); setSortAsc(true); } } function SortIcon({ col }: { col: SortKey }) { if (sortKey !== col) return null; return sortAsc ? ( ) : ( ); } const thStyle: React.CSSProperties = { padding: "7px 8px", fontSize: 11, color: "#5b6066", textTransform: "uppercase", letterSpacing: "0.04em", fontWeight: 600, borderBottom: "1px solid #e6e8ec", textAlign: "left", whiteSpace: "nowrap", background: "#f9fafb", }; const tdStyle: React.CSSProperties = { padding: "8px 8px", fontSize: 13, color: "#1a1d23", borderBottom: "1px solid #f3f4f6", verticalAlign: "middle", }; const numTd: React.CSSProperties = { ...tdStyle, fontVariantNumeric: "tabular-nums", textAlign: "right", }; const sortableTh = (col: SortKey): React.CSSProperties => ({ ...thStyle, cursor: "pointer", userSelect: "none", color: sortKey === col ? "#1d4ed8" : "#5b6066", }); return (
{sorted.map((row, i) => { const imgSrc = safeImgSrc(row.photo_url); return ( ); })}
Фото Адрес м² Комн. Этаж handleSort("price_rub")} > Цена ₽ handleSort("price_per_m2")} > ₽/м² handleSort("days_on_market")} > Дней
{imgSrc ? ( // eslint-disable-next-line @next/next/no-img-element фото ) : (
)}
{row.address} {row.listing_date && ( {fmtDate(row.listing_date)} )} {row.area_m2.toFixed(1)} {row.rooms === 0 ? "ст." : row.rooms} {row.floor !== null && row.total_floors !== null ? `${row.floor}/${row.total_floors}` : (row.floor ?? "—")} {fmtRub(row.price_rub)} {fmtRub(row.price_per_m2)} {row.days_on_market !== null ? row.days_on_market : "—"}
); }