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:
egecankomur
2026-05-05 12:03:48 +03:00
parent 2f17c342ca
commit 4ef0482732
34 changed files with 3174 additions and 36 deletions
+138
View File
@@ -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>
);
}