439 lines
20 KiB
TypeScript
439 lines
20 KiB
TypeScript
import { getTranslations, setRequestLocale } from 'next-intl/server'
|
||
import { mockDb } from '@/lib/mockDb'
|
||
import ListingCard from '@/components/ListingCard'
|
||
import { notFound } from 'next/navigation'
|
||
import Image from 'next/image'
|
||
import { Link } from '@/i18n/routing'
|
||
import { Phone, Globe, MapPin, Clock, Star, MessageSquare, Share2 } from 'lucide-react'
|
||
import SaveButton from './SaveButton'
|
||
import DetailTracker from './DetailTracker'
|
||
|
||
interface DetailPageProps {
|
||
params: Promise<{ locale: string; category: string; slug: string }>
|
||
}
|
||
|
||
export const dynamic = 'force-dynamic'
|
||
|
||
export async function generateMetadata({ params }: DetailPageProps) {
|
||
const { slug } = await params
|
||
const listing = await mockDb.getListingBySlug(slug)
|
||
if (!listing) return {}
|
||
|
||
return {
|
||
title: `${listing.nameTr} — Marmaris Local`,
|
||
description: listing.descriptionTr
|
||
}
|
||
}
|
||
|
||
export default async function ListingDetailPage({ params }: DetailPageProps) {
|
||
const { locale, category, slug } = await params
|
||
setRequestLocale(locale)
|
||
|
||
const t = await getTranslations('detail')
|
||
|
||
const listing = await mockDb.getListingBySlug(slug)
|
||
if (!listing) {
|
||
notFound()
|
||
}
|
||
|
||
// Fetch similar listings in same category (limit to 3, excluding current)
|
||
const allCategoryListings = await mockDb.getListings({
|
||
categoryId: listing.categoryId
|
||
})
|
||
const relatedListings = allCategoryListings
|
||
.filter(l => l.id !== listing.id)
|
||
.slice(0, 3)
|
||
|
||
// Fetch newest 3 listings (excluding current)
|
||
const allListings = await mockDb.getListings()
|
||
const latestListings = allListings
|
||
.filter(l => l.id !== listing.id)
|
||
.slice(0, 3)
|
||
|
||
// Fetch Instagram Feed Cache
|
||
const instagramFeed = await mockDb.getInstagramFeedCacheByListingId(listing.id)
|
||
const instagramPosts = instagramFeed?.posts ? (instagramFeed.posts as any[]) : []
|
||
|
||
// Localized values
|
||
const name =
|
||
locale === 'ru'
|
||
? listing.nameRu
|
||
: locale === 'en'
|
||
? listing.nameEn
|
||
: listing.nameTr
|
||
|
||
const description =
|
||
locale === 'ru'
|
||
? listing.descriptionRu
|
||
: locale === 'en'
|
||
? listing.descriptionEn
|
||
: listing.descriptionTr
|
||
|
||
const priceSymbols = '₺'.repeat(listing.priceRange)
|
||
const categorySlug = listing.category?.slug || 'isletme'
|
||
const rawId = listing.id; // örn: "gm-ChIJY_2Q8tbJvxQRs7xwaSne0is"
|
||
const hasGooglePlaceId = rawId.startsWith('gm-');
|
||
const placeId = hasGooglePlaceId ? rawId.replace('gm-', '') : null;
|
||
// Format WhatsApp Link
|
||
const getWhatsAppLink = (number: string) => {
|
||
const cleanNum = number.replace(/\D/g, '')
|
||
return `https://wa.me/${cleanNum}`
|
||
}
|
||
|
||
// WhatsApp Share deep link text
|
||
const getShareLink = () => {
|
||
const text = encodeURIComponent(`Marmaris Local'da harika bir yer keşfettim: ${name}\nDetayları incele: https://marmarislocal.com/${locale}/${categorySlug}/${listing.slug}`)
|
||
return `https://wa.me/?text=${text}`
|
||
}
|
||
|
||
return (
|
||
<div className="flex-1 flex flex-col min-h-screen bg-stone text-ink">
|
||
<DetailTracker listingId={listing.id} />
|
||
|
||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 flex-1 space-y-10">
|
||
|
||
{/* Breadcrumb */}
|
||
<div className="text-xs font-mono uppercase tracking-wider text-shutter flex items-center gap-2">
|
||
<span className="hover:text-turquoise transition-colors">marmaris local</span>
|
||
<span>/</span>
|
||
<span className="hover:text-turquoise transition-colors">
|
||
{locale === 'ru' ? listing.category?.nameRu : locale === 'en' ? listing.category?.nameEn : listing.category?.nameTr}
|
||
</span>
|
||
<span>/</span>
|
||
<span className="text-pine font-bold">{name}</span>
|
||
</div>
|
||
|
||
{/* Hero Details Block */}
|
||
<div className="bg-paper rounded-3xl border border-pine/8 p-6 sm:p-10 shadow-sm space-y-8">
|
||
<div className="flex flex-col lg:flex-row gap-10">
|
||
|
||
{/* Left: Gallery Panel */}
|
||
<div className="flex-1 space-y-4">
|
||
<div className="aspect-[16/10] w-full relative rounded-2xl overflow-hidden bg-stone-deep shadow-sm">
|
||
<Image
|
||
src={listing.images && listing.images.length > 0 ? listing.images[0].url : 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&auto=format&fit=crop&q=80'}
|
||
alt={name}
|
||
fill
|
||
priority
|
||
className="object-cover"
|
||
/>
|
||
</div>
|
||
|
||
{/* Thumbnails if multiple images exist */}
|
||
{listing.images && listing.images.length > 1 && (
|
||
<div className="grid grid-cols-4 gap-4">
|
||
{listing.images.slice(1, 5).map((img, idx) => (
|
||
<div key={img.id} className="aspect-square relative rounded-xl overflow-hidden bg-stone-deep border border-pine/5 shadow-sm">
|
||
<Image
|
||
src={img.url}
|
||
alt={`${name} thumbnail ${idx + 1}`}
|
||
fill
|
||
className="object-cover hover:scale-105 transition duration-300"
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Right: Info Panel */}
|
||
<div className="flex-1 flex flex-col justify-between space-y-6">
|
||
<div className="space-y-4">
|
||
<div className="flex flex-wrap gap-2.5 items-center">
|
||
<span className="text-[10px] font-mono text-bougainvillea font-bold uppercase tracking-wider bg-bougainvillea/5 border border-bougainvillea/10 px-2.5 py-1 rounded-full">
|
||
{locale === 'ru' ? listing.category?.nameRu : locale === 'en' ? listing.category?.nameEn : listing.category?.nameTr}
|
||
</span>
|
||
|
||
{listing.isLocalApproved && (
|
||
<span className="inline-flex items-center gap-1 text-[10px] font-mono text-turquoise font-bold uppercase tracking-wider bg-turquoise/5 border border-turquoise/10 px-2.5 py-1 rounded-full">
|
||
★ {t('approved')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<h1 className="font-heading font-extrabold text-2xl sm:text-4xl text-pine leading-tight lowercase">
|
||
{name}
|
||
</h1>
|
||
|
||
{/* Stars and Price level */}
|
||
<div className="flex items-center gap-6 font-mono text-sm border-b border-dashed border-pine/8 pb-4">
|
||
{listing.rating && (
|
||
<div className="flex items-center gap-1.5 text-gold font-bold">
|
||
<Star className="w-4 h-4 fill-gold stroke-gold" />
|
||
<span>{t('rating')}: {listing.rating.toFixed(1)}</span>
|
||
</div>
|
||
)}
|
||
<div className="text-pine font-semibold">
|
||
<span>{t('price')}: {priceSymbols}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Description */}
|
||
<div className="text-ink/80 text-sm sm:text-base leading-relaxed font-medium">
|
||
{description ? description : <span className="italic text-ink/50">{t('no_description', { defaultValue: 'Bu işletme için henüz bir açıklama eklenmemiştir.' })}</span>}
|
||
</div>
|
||
|
||
{/* Contact Info (Only if at least one exists) */}
|
||
{(listing.phone || listing.website || listing.instagram) && (
|
||
<div className="space-y-4 pt-6 border-t border-pine/10">
|
||
<h3 className="font-heading font-bold text-xs text-pine uppercase tracking-wider">{t('contact')}</h3>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
{listing.phone && (
|
||
<a id="listing-contact-phone" href={`tel:${listing.phone}`} className="flex items-center gap-3 p-3 rounded-xl border border-pine/10 hover:border-turquoise/30 hover:bg-turquoise/5 transition-colors group">
|
||
<div className="w-8 h-8 rounded-full bg-pine/5 flex items-center justify-center group-hover:bg-turquoise/10 transition-colors">
|
||
<Phone className="w-4 h-4 text-pine group-hover:text-turquoise transition-colors" />
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<span className="text-[10px] font-bold text-pine/50 uppercase tracking-wider">{t('phone')}</span>
|
||
<span className="text-sm font-medium text-ink group-hover:text-pine transition-colors">{listing.phone}</span>
|
||
</div>
|
||
</a>
|
||
)}
|
||
{listing.website && (
|
||
<a id="listing-contact-website" href={listing.website.startsWith('http') ? listing.website : `https://${listing.website}`} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 p-3 rounded-xl border border-pine/10 hover:border-turquoise/30 hover:bg-turquoise/5 transition-colors group">
|
||
<div className="w-8 h-8 rounded-full bg-pine/5 flex items-center justify-center group-hover:bg-turquoise/10 transition-colors">
|
||
<Globe className="w-4 h-4 text-pine group-hover:text-turquoise transition-colors" />
|
||
</div>
|
||
<div className="flex flex-col">
|
||
<span className="text-[10px] font-bold text-pine/50 uppercase tracking-wider">{t('website')}</span>
|
||
<span className="text-sm font-medium text-ink group-hover:text-pine transition-colors truncate max-w-[150px]">
|
||
{listing.website.replace(/^https?:\/\//, '').replace(/\/$/, '')}
|
||
</span>
|
||
</div>
|
||
</a>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Contact & Actions Grid */}
|
||
<div className="space-y-4 pt-2">
|
||
<h3 className="font-heading font-bold text-xs uppercase tracking-wider text-shutter">{t('contact')}</h3>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
{listing.phone && (
|
||
<a
|
||
id="listing-contact-phone"
|
||
href={`tel:${listing.phone}`}
|
||
className="flex items-center justify-center gap-2 bg-pine hover:bg-pine/90 text-stone font-bold text-xs py-3.5 px-4 rounded-xl transition"
|
||
>
|
||
<Phone className="w-4 h-4" />
|
||
{t('call')}
|
||
</a>
|
||
)}
|
||
|
||
{listing.whatsapp && (
|
||
<a
|
||
id="listing-contact-whatsapp"
|
||
href={getWhatsAppLink(listing.whatsapp)}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex items-center justify-center gap-2 bg-turquoise hover:bg-turquoise/90 text-paper font-bold text-xs py-3.5 px-4 rounded-xl transition"
|
||
>
|
||
<MessageSquare className="w-4 h-4" />
|
||
{t('whatsapp')}
|
||
</a>
|
||
)}
|
||
|
||
{/* Menu Link */}
|
||
{listing.menuUrl && (
|
||
<a
|
||
id="listing-contact-menu"
|
||
href={listing.menuUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex items-center justify-center gap-2 bg-paper text-pine font-bold text-xs py-3.5 px-4 rounded-xl border border-pine/15 hover:bg-stone/50 transition sm:col-span-2"
|
||
>
|
||
<Globe className="w-4 h-4 text-turquoise" />
|
||
{t('viewMenu')}
|
||
</a>
|
||
)}
|
||
|
||
{/* Save Button (Favorites client action) */}
|
||
<SaveButton
|
||
listingId={listing.id}
|
||
saveLabel={t('save')}
|
||
savedLabel={t('saved')}
|
||
/>
|
||
</div>
|
||
|
||
{/* External links */}
|
||
<div className="flex flex-wrap gap-5 pt-2 text-xs font-semibold text-shutter">
|
||
{listing.website && (
|
||
<a href={listing.website} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5 hover:text-turquoise transition">
|
||
<Globe className="w-4 h-4 text-turquoise" />
|
||
{t('website')}
|
||
</a>
|
||
)}
|
||
|
||
{listing.instagram && (
|
||
<a href={`https://instagram.com/${listing.instagram}`} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5 hover:text-turquoise transition">
|
||
<Globe className="w-4 h-4 text-turquoise" />
|
||
{t('instagram')}
|
||
</a>
|
||
)}
|
||
|
||
{/* WhatsApp Share button */}
|
||
<a href={getShareLink()} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1.5 hover:text-turquoise transition">
|
||
<Share2 className="w-4 h-4 text-turquoise" />
|
||
{t('shareWhatsapp')}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Details footer (Hours & Location map) */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 pt-8 border-t border-dashed border-pine/12">
|
||
|
||
{/* Address */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 font-heading font-bold text-xs text-pine uppercase tracking-wider">
|
||
<MapPin className="w-4 h-4 text-turquoise" />
|
||
<span>{t('address')}</span>
|
||
</div>
|
||
<p className="text-xs text-ink/75 font-medium leading-relaxed">
|
||
{listing.address}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Hours */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 font-heading font-bold text-xs text-pine uppercase tracking-wider">
|
||
<Clock className="w-4 h-4 text-turquoise" />
|
||
<span>{t('hours')}</span>
|
||
</div>
|
||
<div className="text-xs text-ink/75 font-medium">
|
||
{listing.openingHours ? (
|
||
<p>{(listing.openingHours as any).all || t('noHours')}</p>
|
||
) : (
|
||
<p>{t('noHours')}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Map Frame */}
|
||
{((listing.latitude && listing.longitude) || hasGooglePlaceId) && (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 font-heading font-bold text-xs text-pine uppercase tracking-wider">
|
||
<Globe className="w-4 h-4 text-turquoise" />
|
||
<span>{t('location')}</span>
|
||
</div>
|
||
<div className="rounded-xl overflow-hidden border border-pine/8 aspect-[16/10] sm:aspect-auto sm:h-36 relative group">
|
||
<iframe
|
||
width="100%"
|
||
height="100%"
|
||
frameBorder="0"
|
||
scrolling="no"
|
||
marginHeight={0}
|
||
marginWidth={0}
|
||
src={
|
||
hasGooglePlaceId
|
||
? `https://maps.google.com/maps?q=place_id:${placeId}&z=16&output=embed`
|
||
: `https://maps.google.com/maps?q=${listing.latitude},${listing.longitude}+(${encodeURIComponent(name)})&t=&z=16&ie=UTF8&output=embed`
|
||
}
|
||
className="w-full h-full shadow-sm"
|
||
loading="lazy"
|
||
referrerPolicy="no-referrer-when-downgrade"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Instagram Feed Cache (Phase 2) */}
|
||
{instagramFeed && instagramPosts.length > 0 && (
|
||
<div className="bg-paper rounded-3xl border border-pine/8 p-6 sm:p-10 shadow-sm space-y-6">
|
||
<div className="flex items-center gap-3 border-b border-dashed border-pine/8 pb-4">
|
||
<div className="w-10 h-10 rounded-full border border-pine/10 flex items-center justify-center relative bg-stone shrink-0">
|
||
<span className="font-heading font-bold text-pine text-xs tracking-tighter">IG</span>
|
||
</div>
|
||
<div>
|
||
<h3 className="font-heading font-bold text-sm text-pine lowercase">@{instagramFeed.handle}</h3>
|
||
<p className="text-[10px] font-mono text-shutter uppercase tracking-wider mt-0.5">{t('instagramFeed')}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||
{instagramPosts.slice(0, 3).map((post: any, idx: number) => (
|
||
<a
|
||
key={idx}
|
||
href={post.permalink || '#'}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="group relative aspect-square rounded-2xl overflow-hidden bg-stone border border-pine/5 shadow-sm block"
|
||
>
|
||
<img
|
||
src={post.imageUrl}
|
||
alt={post.caption || 'Instagram post'}
|
||
className="w-full h-full object-cover group-hover:scale-103 transition duration-500"
|
||
/>
|
||
<div className="absolute inset-0 bg-pine/70 opacity-0 group-hover:opacity-100 transition-opacity duration-300 p-4 flex flex-col justify-end">
|
||
<p className="text-[11px] text-stone font-medium line-clamp-3 leading-relaxed">
|
||
{post.caption}
|
||
</p>
|
||
</div>
|
||
</a>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Newly Added Widget (Phase 2) */}
|
||
{latestListings.length > 0 && (
|
||
<div className="bg-paper rounded-3xl border border-pine/8 p-6 sm:p-10 shadow-sm space-y-6">
|
||
<div className="border-b border-dashed border-pine/8 pb-4">
|
||
<h3 className="text-lg font-heading font-extrabold text-pine lowercase">
|
||
{t('newlyAdded')}
|
||
</h3>
|
||
<p className="text-ink/65 text-xs font-medium mt-0.5">
|
||
{t('newlyAddedSubtitle')}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||
{latestListings.map((newest) => {
|
||
const catSlug = newest.category?.slug || 'businesses'
|
||
const newestName = locale === 'en' ? newest.nameEn : locale === 'ru' ? newest.nameRu : newest.nameTr
|
||
const newestImg = newest.images && newest.images.length > 0 ? newest.images[0].url : 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&auto=format&fit=crop&q=80'
|
||
return (
|
||
<Link
|
||
key={newest.id}
|
||
href={`/${catSlug}/${newest.slug}`}
|
||
className="flex gap-4 items-center group bg-stone/20 p-3.5 rounded-2xl border border-pine/5 hover:border-turquoise/25 transition duration-150"
|
||
>
|
||
<div className="w-16 h-16 rounded-xl overflow-hidden bg-stone shrink-0 border border-pine/8">
|
||
<img src={newestImg} alt={newestName} className="w-full h-full object-cover group-hover:scale-105 transition duration-300" />
|
||
</div>
|
||
<div>
|
||
<h4 className="font-heading font-bold text-xs text-pine lowercase line-clamp-1 group-hover:text-turquoise transition-colors">{newestName}</h4>
|
||
<p className="text-[10px] text-shutter font-mono mt-0.5">{newest.neighborhood?.nameTr}</p>
|
||
</div>
|
||
</Link>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Related Section */}
|
||
{relatedListings.length > 0 && (
|
||
<div className="space-y-6 pt-6">
|
||
<h3 className="text-xl font-heading font-extrabold text-pine lowercase border-b border-dashed border-pine/8 pb-3">
|
||
{t('related')}
|
||
</h3>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||
{relatedListings.map((listingItem) => (
|
||
<ListingCard key={listingItem.id} listing={listingItem} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|