diff --git a/src/app/(dashboard)/academy/page.tsx b/src/app/(dashboard)/academy/page.tsx index 33f0bb9..8952d8b 100644 --- a/src/app/(dashboard)/academy/page.tsx +++ b/src/app/(dashboard)/academy/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import { GraduationCap } from '@/lib/icons'; import { AcademyClient } from "@/components/academy/academy-client"; @@ -5,7 +7,7 @@ export const metadata = { title: "Akademi | KovakEmlak" }; export default function AcademyPage() { return ( -
+
diff --git a/src/app/(dashboard)/finance/page.tsx b/src/app/(dashboard)/finance/page.tsx index cc50a23..3d40f9c 100644 --- a/src/app/(dashboard)/finance/page.tsx +++ b/src/app/(dashboard)/finance/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import { requireTenant } from "@/lib/appwrite/tenant-guard"; import { createAdminClient } from "@/lib/appwrite/server"; import { DATABASE_ID, TABLES } from "@/lib/appwrite/schema"; diff --git a/src/app/(dashboard)/settings/billing/page.tsx b/src/app/(dashboard)/settings/billing/page.tsx index 5fe49ba..4c766a3 100644 --- a/src/app/(dashboard)/settings/billing/page.tsx +++ b/src/app/(dashboard)/settings/billing/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { CheckCircle, XCircle } from '@/lib/icons'; diff --git a/src/app/(dashboard)/settings/members/page.tsx b/src/app/(dashboard)/settings/members/page.tsx index 0422054..8bc2038 100644 --- a/src/app/(dashboard)/settings/members/page.tsx +++ b/src/app/(dashboard)/settings/members/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import type { Metadata } from "next"; import { redirect } from "next/navigation"; import { Query } from "node-appwrite"; diff --git a/src/app/(dashboard)/settings/workspace/page.tsx b/src/app/(dashboard)/settings/workspace/page.tsx index 615e3cb..eefc626 100644 --- a/src/app/(dashboard)/settings/workspace/page.tsx +++ b/src/app/(dashboard)/settings/workspace/page.tsx @@ -1,3 +1,5 @@ +export const dynamic = "force-dynamic"; + import type { Metadata } from "next"; import { redirect } from "next/navigation"; diff --git a/src/app/sunum/[token]/page.tsx b/src/app/sunum/[token]/page.tsx index 18edd71..4b304c2 100644 --- a/src/app/sunum/[token]/page.tsx +++ b/src/app/sunum/[token]/page.tsx @@ -1,16 +1,11 @@ import { notFound } from "next/navigation"; import { Query } from "node-appwrite"; -import { Buildings, MapPin, House } from '@/lib/icons'; +import { Buildings, House } from '@/lib/icons'; 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"; -import { getPropertyImagePreviewUrl, parseImageIds } from "@/lib/appwrite/storage-utils"; +import { SunumClient } from "./sunum-client"; interface Props { params: Promise<{ token: string }>; @@ -102,15 +97,14 @@ export default async function SunumPage({ params }: Props) { {/* Grid */} -
- {properties.map((p) => ( - - ))} - {properties.length === 0 && ( -
+
+ {properties.length === 0 ? ( +

Bu sunumda ilan bulunmuyor.

+ ) : ( + )}
@@ -131,113 +125,3 @@ export default async function SunumPage({ params }: Props) {
); } - -function PropertyCard({ property: p }: { property: Property }) { - const isSold = p.status === "satildi" || p.status === "kiralandit"; - const imageIds = parseImageIds(p.imageIds); - const coverImageId = imageIds[0]; - - const statusStyle: Record = { - aktif: "bg-green-500/90 text-white", - pasif: "bg-gray-500/80 text-white", - satildi: "bg-orange-500/90 text-white", - kiralandit: "bg-blue-500/90 text-white", - }; - - return ( -
- {/* Photo */} -
- {coverImageId ? ( - // eslint-disable-next-line @next/next/no-img-element - {p.title} - ) : ( -
- -
- )} - - {/* Status badge — top left */} - - {PROPERTY_STATUS_LABELS[p.status] ?? p.status} - - - {/* Listing type — top right */} - - {LISTING_TYPE_LABELS[p.listingType] ?? p.listingType} - - - {/* Photo count pill */} - {imageIds.length > 1 && ( - - {imageIds.length} fotoğraf - - )} -
- - {/* Body */} -
-

{p.title}

- - {/* Location */} - {(p.city || p.district || p.neighborhood) && ( -
- - - {[p.neighborhood, p.district, p.city].filter(Boolean).join(", ")} - -
- )} - - {/* Spec pills */} -
- - {PROPERTY_TYPE_LABELS[p.propertyType] ?? p.propertyType} - - {p.roomCount && ( - - {p.roomCount} - - )} - {p.netM2 && ( - - {p.netM2} m² - - )} -
- - {/* Price — pushed to bottom */} -
-

- {p.price.toLocaleString("tr-TR")} - {p.currency ?? "TRY"} -

-
- - {p.description && ( -

{p.description}

- )} - - {p.mapLat != null && p.mapLng != null && ( - - - Haritada gör - - )} -
-
- ); -} diff --git a/src/app/sunum/[token]/sunum-client.tsx b/src/app/sunum/[token]/sunum-client.tsx new file mode 100644 index 0000000..b77008b --- /dev/null +++ b/src/app/sunum/[token]/sunum-client.tsx @@ -0,0 +1,468 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { createPortal } from "react-dom"; +import { X, CaretLeft, CaretRight, MapPin, House, ImageSquare } from "@/lib/icons"; +import { + getPropertyImageUrl, + getPropertyImagePreviewUrl, + parseImageIds, +} from "@/lib/appwrite/storage-utils"; +import { + PROPERTY_TYPE_LABELS, + LISTING_TYPE_LABELS, + PROPERTY_STATUS_LABELS, + type Property, +} from "@/lib/appwrite/schema"; + +/* ── Status style map ── */ +const STATUS_STYLE: Record = { + aktif: "bg-green-500/90 text-white", + pasif: "bg-gray-500/80 text-white", + satildi: "bg-orange-500/90 text-white", + kiralandit: "bg-blue-500/90 text-white", +}; + +/* ── Mini gallery (card içi) ── */ +function CardGallery({ imageIds, title, onOpenLightbox, onOpenDetail }: { + imageIds: string[]; + title: string; + onOpenLightbox: (idx: number) => void; + onOpenDetail: () => void; +}) { + const [idx, setIdx] = useState(0); + const safeIdx = Math.min(idx, imageIds.length - 1); + + if (imageIds.length === 0) { + return ( + + ); + } + + return ( + <> + {/* Ana görsel */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {title} onOpenLightbox(safeIdx)} + /> + {/* Prev / Next */} + {imageIds.length > 1 && ( + <> + + +
+ {safeIdx + 1}/{imageIds.length} +
+ + )} +
+ + {/* Thumbnail şeridi */} + {imageIds.length > 1 && ( +
+ {imageIds.map((id, i) => ( + + ))} +
+ )} + + ); +} + +/* ── Lightbox ── */ +function Lightbox({ imageIds, title, initialIndex, onClose }: { + imageIds: string[]; + title: string; + initialIndex: number; + onClose: () => void; +}) { + const [idx, setIdx] = useState(initialIndex); + const prev = useCallback(() => setIdx((i) => (i - 1 + imageIds.length) % imageIds.length), [imageIds.length]); + const next = useCallback(() => setIdx((i) => (i + 1) % imageIds.length), [imageIds.length]); + + return createPortal( +
+ +
+ {idx + 1} / {imageIds.length} +
+ {imageIds.length > 1 && ( + + )} + {/* eslint-disable-next-line @next/next/no-img-element */} + {`${title} e.stopPropagation()} + draggable={false} + /> + {imageIds.length > 1 && ( + + )} + {imageIds.length > 1 && ( +
e.stopPropagation()} + > + {imageIds.map((id, i) => ( + + ))} +
+ )} +
, + document.body, + ); +} + +/* ── Detay Modal ── */ +function DetailModal({ p, onClose }: { p: Property; onClose: () => void }) { + const imageIds = parseImageIds(p.imageIds); + const [activeIdx, setActiveIdx] = useState(0); + const [lightboxOpen, setLightboxOpen] = useState(false); + + return createPortal( +
+
e.stopPropagation()} + > + {/* Galeri */} +
+ {imageIds.length > 0 ? ( + <> +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {p.title} setLightboxOpen(true)} + /> + {imageIds.length > 1 && ( + <> + + +
+ {activeIdx + 1} / {imageIds.length} +
+ + )} +
+ {/* Thumbnail şeridi */} + {imageIds.length > 1 && ( +
+ {imageIds.map((id, i) => ( + + ))} +
+ )} + + ) : ( +
+ +
+ )} + + {/* Kapat */} + +
+ + {/* İçerik */} +
+ {/* Başlık + durum */} +
+

{p.title}

+ + {PROPERTY_STATUS_LABELS[p.status] ?? p.status} + +
+ + {/* Fiyat */} +

+ {p.price.toLocaleString("tr-TR")} + {p.currency ?? "TRY"} +

+ + {/* Konum */} + {(p.city || p.district || p.neighborhood) && ( +
+ + {[p.neighborhood, p.district, p.city].filter(Boolean).join(", ")} +
+ )} + + {/* Özellik pilleri */} +
+ + {PROPERTY_TYPE_LABELS[p.propertyType] ?? p.propertyType} + + + {LISTING_TYPE_LABELS[p.listingType] ?? p.listingType} + + {p.roomCount && ( + {p.roomCount} + )} + {p.netM2 && ( + {p.netM2} m² + )} + {p.grossM2 && ( + {p.grossM2} m² brüt + )} + {p.floor != null && ( + {p.floor}. kat + )} + {p.totalFloors != null && ( + {p.totalFloors} katlı + )} + {p.buildingAge != null && ( + {p.buildingAge === 0 ? "Sıfır bina" : `${p.buildingAge} yaşında`} + )} +
+ + {/* Açıklama */} + {p.description && ( +
+

Açıklama

+

{p.description}

+
+ )} + + {/* Harita linki */} + {p.mapLat != null && p.mapLng != null && ( + + )} +
+
+ + {lightboxOpen && ( + setLightboxOpen(false)} + /> + )} +
, + document.body, + ); +} + +/* ── Ana kart ── */ +function PropertyCard({ p }: { p: Property }) { + const imageIds = parseImageIds(p.imageIds); + const [lightboxIdx, setLightboxIdx] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); + const isSold = p.status === "satildi" || p.status === "kiralandit"; + + return ( + <> +
+ {/* Görsel alanı */} +
1 ? "calc(13rem + 2.75rem)" : "13rem" }}> +
+ setLightboxIdx(idx)} + onOpenDetail={() => setDetailOpen(true)} + /> +
+ + {/* Statü */} + + {PROPERTY_STATUS_LABELS[p.status] ?? p.status} + + + {/* Listing type */} + + {LISTING_TYPE_LABELS[p.listingType] ?? p.listingType} + +
+ + {/* Body */} +
+ + + {(p.city || p.district || p.neighborhood) && ( +
+ + + {[p.neighborhood, p.district, p.city].filter(Boolean).join(", ")} + +
+ )} + +
+ + {PROPERTY_TYPE_LABELS[p.propertyType] ?? p.propertyType} + + {p.roomCount && ( + {p.roomCount} + )} + {p.netM2 && ( + {p.netM2} m² + )} +
+ +
+

+ {p.price.toLocaleString("tr-TR")} + {p.currency ?? "TRY"} +

+ +
+
+
+ + {lightboxIdx !== null && ( + setLightboxIdx(null)} + /> + )} + + {detailOpen && ( + setDetailOpen(false)} /> + )} + + ); +} + +/* ── Export ── */ +export function SunumClient({ properties }: { properties: Property[] }) { + return ( +
+ {properties.map((p) => ( + + ))} +
+ ); +} diff --git a/src/components/properties/property-form-sheet.tsx b/src/components/properties/property-form-sheet.tsx index ffe6858..2558174 100644 --- a/src/components/properties/property-form-sheet.tsx +++ b/src/components/properties/property-form-sheet.tsx @@ -159,9 +159,9 @@ export function PropertyFormSheet({ open, onOpenChange, property, onSuccess }: P { label: "Özellikler", content: ( -
+
- +
- +
-
- +
+
diff --git a/src/lib/academy/tours.ts b/src/lib/academy/tours.ts index 786f98b..a117575 100644 --- a/src/lib/academy/tours.ts +++ b/src/lib/academy/tours.ts @@ -69,20 +69,19 @@ export const ACADEMY_MODULES: AcademyModule[] = [ { element: "[data-tour='properties-table']", title: "İlan Tablosu", - description: "Tüm ilanlar burada listelenir. Bir satıra tıklayarak detay sayfasına geçebilir, sağdaki menüden düzenleyebilir veya silebilirsiniz.", + description: "Tüm ilanlar burada listelenir. Her satırın solunda ilan görseli yer alır — görsele tıklayınca tam ekran galeri açılır. Satırın herhangi bir yerine tıklayarak detay sayfasına geçebilir, sağdaki menüden düzenleyebilir veya silebilirsiniz.", side: "top", }, { element: "[data-tour='properties-add']", - title: "Yeni İlan Ekle", - description: "Bu düğmeyle 4 adımlı ilan ekleme formu açılır: İlan bilgileri → Konum → Özellikler → Medya. Şimdi formun içini inceleyelim.", + title: "Görünüm Modları & Yeni İlan", + description: "Sağ üstteki Liste / Galeri / Harita düğmeleriyle portföyü farklı görünümlerde inceleyebilirsiniz. Galeri modunda her ilanın fotoğraflarını doğrudan listeden gezebilirsiniz. 'Yeni İlan' ile 4 adımlı form açılır.", side: "bottom", - // next click opens form }, { element: "[data-tour='form-step-nav']", title: "Form Adımları — Genel Bakış", - description: "Form 4 adımdan oluşur. Dairelere tıklayarak adımlar arasında serbestçe geçiş yapabilirsiniz. Tüm alanlara girdiğiniz veriler her adımda korunur.", + description: "Form 4 adımdan oluşur: İlan Bilgileri → Konum → Özellikler → Medya. Dairelere tıklayarak adımlar arasında serbestçe geçiş yapabilirsiniz.", side: "bottom", triggerEvent: "kovak:open-form-properties", triggerDelay: 750, @@ -112,7 +111,7 @@ export const ACADEMY_MODULES: AcademyModule[] = [ { element: "[data-tour='form-step-content-3']", title: "4. Adım — Medya", - description: "İlan açıklamasını yazın ve fotoğraf yükleyin. Birden fazla fotoğraf sürükleyip sıralayabilirsiniz. Son adımda 'Oluştur' ile kaydedilir.", + description: "İlan açıklamasını yazın ve fotoğraf yükleyin. Fotoğrafları sürükle-bırak ile ekleyebilirsiniz — yükleme sırasında otomatik sıkıştırılır (maks. 1920px). Son adımda 'Oluştur' ile kaydedilir.", side: "bottom", clickBefore: "[data-tour='form-circle-3']", clickDelay: 350,