Files
kovakemlak-crm/src/app/sunum/[token]/page.tsx
T
egecankomur 4ef0482732 feat: all core modules — properties, customers, searches, matches, presentations, activities, investors + public sunum page
- 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
2026-05-05 12:03:48 +03:00

139 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}