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
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user