Files
marmarislocal/app/[locale]/[category]/[slug]/page.tsx
T

391 lines
17 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 { 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'
interface DetailPageProps {
params: Promise<{ locale: string; category: string; slug: string }>
}
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'
// 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">
<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}
</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
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
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
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 && (
<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">
<iframe
width="100%"
height="100%"
frameBorder="0"
scrolling="no"
marginHeight={0}
marginWidth={0}
src={`https://maps.google.com/maps?q=${listing.latitude},${listing.longitude}&t=&z=15&ie=UTF8&iwloc=&output=embed`}
className="w-full h-full shadow-sm"
/>
</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 || 'isletmeler'
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>
)
}