feat: desktop image thumbnails, gallery lightbox portal, client-side compression, clickable table rows, fix header gap

This commit is contained in:
egecankomur
2026-05-12 04:49:36 +03:00
parent 3cce632eb3
commit 3554b39800
134 changed files with 7736 additions and 1913 deletions
+9 -219
View File
@@ -1,8 +1,6 @@
export const dynamic = "force-dynamic";
import { notFound } from "next/navigation";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
import { Query } from "node-appwrite";
import { requireTenant } from "@/lib/appwrite/tenant-guard";
@@ -10,18 +8,13 @@ import { listCustomers } from "@/lib/appwrite/customer-queries";
import {
DATABASE_ID,
TABLES,
PROPERTY_TYPE_LABELS,
LISTING_TYPE_LABELS,
PROPERTY_STATUS_LABELS,
ACTIVITY_TYPE_LABELS,
type Property,
type PropertyMatch,
type Activity,
} from "@/lib/appwrite/schema";
import { createAdminClient } from "@/lib/appwrite/server";
import { getPropertyImagePreviewUrl, parseImageIds } from "@/lib/appwrite/storage-utils";
import { Badge } from "@/components/ui/badge";
import { PropertyMapView } from "@/components/map/property-map-view";
import { parseImageIds } from "@/lib/appwrite/storage-utils";
import { PropertyDetailClient } from "@/components/properties/property-detail-client";
interface Props {
params: Promise<{ id: string }>;
@@ -71,216 +64,13 @@ export default async function PropertyDetailPage({ params }: Props) {
const customerMap = Object.fromEntries(customers.map((c) => [c.$id, c.name]));
const imageIds = parseImageIds(property.imageIds);
const statusColor: Record<string, string> = {
aktif: "bg-green-100 text-green-700",
pasif: "bg-gray-100 text-gray-600",
satildi: "bg-orange-100 text-orange-700",
kiralandit: "bg-blue-100 text-blue-700",
};
return (
<div className="flex flex-1 flex-col gap-6 p-4 md:p-6 max-w-5xl">
{/* Header */}
<div className="flex items-center gap-3">
<Link
href="/properties"
className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-sm"
>
<ArrowLeft className="size-4" />
İlanlar
</Link>
</div>
<div className="flex items-start justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-bold">{property.title}</h1>
<p className="text-muted-foreground mt-1 text-sm">
{[property.neighborhood, property.district, property.city].filter(Boolean).join(", ")}
</p>
</div>
<span
className={`inline-flex items-center rounded-full px-3 py-1 text-sm font-medium ${statusColor[property.status] ?? "bg-gray-100 text-gray-600"}`}
>
{PROPERTY_STATUS_LABELS[property.status] ?? property.status}
</span>
</div>
{/* Photo gallery */}
{imageIds.length > 0 && (
<div>
<div
className={`grid gap-2 ${
imageIds.length === 1
? "grid-cols-1"
: imageIds.length === 2
? "grid-cols-2"
: "grid-cols-3"
}`}
>
{imageIds.map((fileId, i) => (
<div
key={fileId}
className={`overflow-hidden rounded-lg border bg-muted ${i === 0 && imageIds.length > 2 ? "col-span-2 row-span-2" : ""}`}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={getPropertyImagePreviewUrl(fileId, 1200, 900)}
alt={`${property.title} fotoğraf ${i + 1}`}
className="h-full w-full object-cover"
style={{ maxHeight: i === 0 && imageIds.length > 2 ? "480px" : "240px" }}
/>
</div>
))}
</div>
</div>
)}
<div className="grid gap-6 md:grid-cols-3">
{/* Price */}
<div className="md:col-span-2 space-y-6">
<div className="rounded-lg border p-4 space-y-4">
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold">
{property.price.toLocaleString("tr-TR")}
</span>
<span className="text-muted-foreground">{property.currency ?? "TRY"}</span>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-sm">
<Detail label="Emlak tipi" value={PROPERTY_TYPE_LABELS[property.propertyType]} />
<Detail label="İlan türü" value={LISTING_TYPE_LABELS[property.listingType]} />
{property.roomCount && <Detail label="Oda sayısı" value={property.roomCount} />}
{property.netM2 && <Detail label="Net m²" value={`${property.netM2}`} />}
{property.grossM2 && <Detail label="Brüt m²" value={`${property.grossM2}`} />}
{property.floor != null && <Detail label="Kat" value={String(property.floor)} />}
{property.totalFloors != null && (
<Detail label="Top. kat" value={String(property.totalFloors)} />
)}
{property.buildingAge != null && (
<Detail label="Bina yaşı" value={`${property.buildingAge} yıl`} />
)}
</div>
{property.address && (
<div className="text-sm">
<span className="text-muted-foreground">Adres: </span>
{property.address}
</div>
)}
{property.mapLat != null && property.mapLng != null && (
<a
href={`https://www.google.com/maps?q=${property.mapLat},${property.mapLng}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-600 hover:underline"
>
Google Maps'te
</a>
)}
</div>
{property.description && (
<div className="rounded-lg border p-4">
<h2 className="mb-2 text-sm font-semibold">Açıklama</h2>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
{property.description}
</p>
</div>
)}
{property.mapLat != null && property.mapLng != null && (
<div className="rounded-lg border overflow-hidden">
<h2 className="px-4 pt-4 pb-2 text-sm font-semibold">Konum</h2>
<PropertyMapView
lat={property.mapLat}
lng={property.mapLng}
title={property.title}
className="h-64 rounded-none border-0"
/>
</div>
)}
{/* Activities */}
{activities.length > 0 && (
<div className="rounded-lg border p-4">
<h2 className="mb-3 text-sm font-semibold">Aktiviteler</h2>
<div className="space-y-2">
{activities.map((a) => (
<div key={a.$id} className="flex items-start gap-2 text-sm">
<span className="text-muted-foreground mt-0.5 shrink-0">
{ACTIVITY_TYPE_LABELS[a.type] ?? a.type}
</span>
<div className="min-w-0">
<p className="font-medium truncate">{a.title}</p>
{a.description && (
<p className="text-muted-foreground text-xs truncate">{a.description}</p>
)}
</div>
<span className="text-muted-foreground text-xs ml-auto shrink-0">
{new Date(a.$createdAt).toLocaleDateString("tr-TR")}
</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Matches sidebar */}
<div>
<div className="rounded-lg border p-4">
<h2 className="mb-3 text-sm font-semibold">
İlgili Müşteriler
{matches.length > 0 && (
<span className="ml-1.5 text-muted-foreground font-normal">({matches.length})</span>
)}
</h2>
{matches.length === 0 ? (
<p className="text-muted-foreground text-xs">Eşleşme yok.</p>
) : (
<div className="space-y-2">
{matches.map((m) => (
<div key={m.$id} className="flex items-center justify-between gap-2">
<span className="text-sm truncate">
{customerMap[m.customerId] ?? m.customerId}
</span>
<ScoreBadge score={m.score} />
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
}
function Detail({ label, value }: { label: string; value: string }) {
return (
<div>
<span className="text-muted-foreground">{label}: </span>
<span className="font-medium">{value}</span>
</div>
);
}
function ScoreBadge({ score }: { score?: number | null }) {
const s = score ?? 0;
const color =
s >= 80
? "bg-green-100 text-green-700"
: s >= 60
? "bg-blue-100 text-blue-700"
: s >= 40
? "bg-yellow-100 text-yellow-700"
: "bg-gray-100 text-gray-500";
return (
<span
className={`inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-xs font-semibold ${color}`}
>
{s}
</span>
<PropertyDetailClient
property={property}
matches={matches}
activities={activities}
imageIds={imageIds}
customerMap={customerMap}
/>
);
}