4ef0482732
- Server actions: property/customer/search/presentation/activity/investor CRUD - Matching engine: matchPropertyToSearches + syncMatchesForSearch on search save - UI: form sheets + table clients for all modules - Public /sunum/[token] page (no auth) with property card grid + expiry check - All pages force-dynamic for auth guard compatibility
139 lines
4.5 KiB
TypeScript
139 lines
4.5 KiB
TypeScript
import { notFound } from "next/navigation";
|
||
import { Query } from "node-appwrite";
|
||
|
||
import { DATABASE_ID, TABLES, type Presentation, type Property } from "@/lib/appwrite/schema";
|
||
import { createAdminClient } from "@/lib/appwrite/server";
|
||
import { incrementPresentationViewCount } from "@/lib/appwrite/presentation-actions";
|
||
import {
|
||
PROPERTY_TYPE_LABELS,
|
||
LISTING_TYPE_LABELS,
|
||
PROPERTY_STATUS_LABELS,
|
||
} from "@/lib/appwrite/schema";
|
||
|
||
interface Props {
|
||
params: Promise<{ token: string }>;
|
||
}
|
||
|
||
export default async function SunumPage({ params }: Props) {
|
||
const { token } = await params;
|
||
const { tablesDB } = createAdminClient();
|
||
|
||
const result = await tablesDB.listRows({
|
||
databaseId: DATABASE_ID,
|
||
tableId: TABLES.presentations,
|
||
queries: [Query.equal("shareToken", token), Query.limit(1)],
|
||
});
|
||
|
||
if (!result.rows.length) notFound();
|
||
|
||
const presentation = result.rows[0] as unknown as Presentation;
|
||
|
||
if (presentation.expiresAt && new Date(presentation.expiresAt) < new Date()) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||
<div className="text-center">
|
||
<h1 className="text-2xl font-bold text-gray-800 mb-2">Sunum Süresi Doldu</h1>
|
||
<p className="text-gray-500">Bu sunum artık geçerli değil.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Increment view count (fire-and-forget)
|
||
void incrementPresentationViewCount(presentation.$id, presentation.viewCount ?? 0);
|
||
|
||
let propertyIds: string[] = [];
|
||
try {
|
||
propertyIds = JSON.parse(presentation.propertyIds) as string[];
|
||
} catch {
|
||
propertyIds = [];
|
||
}
|
||
|
||
const properties: Property[] = [];
|
||
for (const pid of propertyIds) {
|
||
try {
|
||
const row = await tablesDB.getRow(DATABASE_ID, TABLES.properties, pid);
|
||
properties.push(row as unknown as Property);
|
||
} catch {
|
||
// Property may have been deleted
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50">
|
||
<header className="bg-white border-b px-6 py-4">
|
||
<h1 className="text-xl font-semibold text-gray-800">{presentation.title}</h1>
|
||
{presentation.notes && (
|
||
<p className="mt-1 text-sm text-gray-500">{presentation.notes}</p>
|
||
)}
|
||
<p className="mt-1 text-xs text-gray-400">{properties.length} ilan</p>
|
||
</header>
|
||
|
||
<main className="max-w-5xl mx-auto px-4 py-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||
{properties.map((p) => (
|
||
<PropertyCard key={p.$id} property={p} />
|
||
))}
|
||
{properties.length === 0 && (
|
||
<p className="col-span-full text-center text-gray-400 py-16">
|
||
Bu sunumda ilan bulunmuyor.
|
||
</p>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function PropertyCard({ property: p }: { property: Property }) {
|
||
const isExpired = p.status === "satildi" || p.status === "kiralandit";
|
||
|
||
return (
|
||
<div className={`bg-white rounded-xl border shadow-sm overflow-hidden ${isExpired ? "opacity-60" : ""}`}>
|
||
<div className="bg-gray-100 h-40 flex items-center justify-center text-4xl text-gray-300">
|
||
🏠
|
||
</div>
|
||
<div className="p-4 space-y-2">
|
||
<div className="flex items-start justify-between gap-2">
|
||
<h2 className="font-semibold text-gray-800 text-sm leading-tight">{p.title}</h2>
|
||
<span className={`text-xs px-1.5 py-0.5 rounded whitespace-nowrap font-medium ${
|
||
p.status === "aktif" ? "bg-green-100 text-green-700" :
|
||
p.status === "pasif" ? "bg-gray-100 text-gray-600" :
|
||
"bg-orange-100 text-orange-700"
|
||
}`}>
|
||
{PROPERTY_STATUS_LABELS[p.status] ?? p.status}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex gap-2 text-xs text-gray-500">
|
||
<span>{PROPERTY_TYPE_LABELS[p.propertyType] ?? p.propertyType}</span>
|
||
<span>·</span>
|
||
<span>{LISTING_TYPE_LABELS[p.listingType] ?? p.listingType}</span>
|
||
{p.roomCount && (
|
||
<>
|
||
<span>·</span>
|
||
<span>{p.roomCount}</span>
|
||
</>
|
||
)}
|
||
{p.netM2 && (
|
||
<>
|
||
<span>·</span>
|
||
<span>{p.netM2} m²</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
<p className="text-xs text-gray-500">
|
||
{[p.neighborhood, p.district, p.city].filter(Boolean).join(", ")}
|
||
</p>
|
||
|
||
<p className="text-lg font-bold text-gray-900">
|
||
{p.price.toLocaleString("tr-TR")} {p.currency ?? "TRY"}
|
||
</p>
|
||
|
||
{p.description && (
|
||
<p className="text-xs text-gray-500 line-clamp-3">{p.description}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|