feat: Implement Phase 2 features including blog, collections, saved listings, manifest, and api cron sync

This commit is contained in:
AyrisAI
2026-07-12 20:49:41 +03:00
parent 862544b5a1
commit f696d359b0
29 changed files with 3066 additions and 194 deletions
+110
View File
@@ -0,0 +1,110 @@
import { mockDb } from '@/lib/mockDb'
import { Link } from '@/i18n/routing'
import { getTranslations } from 'next-intl/server'
import { Calendar, Tag } from 'lucide-react'
export async function generateMetadata() {
return {
title: 'Marmaris Local Blog — Yerel Lezzet ve Gezi Rehberi',
description: 'Marmaris\'i bir yerel gibi keşfetmeniz için rehberler, nerede ne yenir tavsiyeleri ve gizli yerler.'
}
}
export default async function BlogIndexPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
const t = await getTranslations('nav')
const posts = await mockDb.getBlogPosts(true)
return (
<div className="bg-stone min-h-screen py-10 px-4 sm:px-6 lg:px-8 font-sans">
<div className="max-w-5xl mx-auto space-y-10">
{/* Title */}
<div className="text-center space-y-3">
<span className="text-[10px] font-mono tracking-widest text-shutter uppercase bg-paper px-3 py-1.5 rounded-full border border-pine/5">
küratörlü rehberler
</span>
<h1 className="text-4xl font-heading font-extrabold text-pine lowercase">
marmaris local <span className="text-turquoise">blog</span>
</h1>
<p className="max-w-xl mx-auto text-ink/70 text-xs sm:text-sm font-medium">
Turistlerin gözünden kaçan yerel detaylar, en iyi lezzet durakları ve gizli gezi noktaları.
</p>
</div>
{/* Blog Posts Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{posts.length === 0 ? (
<div className="col-span-2 bg-paper border border-pine/8 p-12 text-center text-shutter text-sm rounded-3xl">
Henüz yayınlanmış bir yazı bulunmuyor.
</div>
) : (
posts.map((post) => {
const title = locale === 'en' ? post.titleEn : locale === 'ru' ? post.titleRu : post.titleTr
const content = locale === 'en' ? post.contentEn : locale === 'ru' ? post.contentRu : post.contentTr
const snippet = content.substring(0, 140) + '...'
return (
<article
key={post.id}
className="bg-paper border border-pine/8 rounded-3xl overflow-hidden shadow-sm flex flex-col justify-between hover:border-turquoise/35 transition-all duration-300 hover:shadow-md"
>
<div>
{/* Cover image */}
{post.coverImage && (
<div className="h-56 relative overflow-hidden bg-stone border-b border-pine/5">
<img
src={post.coverImage}
alt={title}
className="w-full h-full object-cover hover:scale-105 transition duration-500"
/>
</div>
)}
{/* Post content preview */}
<div className="p-6 sm:p-8 space-y-4">
{/* Meta info */}
<div className="flex items-center gap-4 text-[10px] text-shutter font-mono">
<span className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5" />
{post.publishedAt ? new Date(post.publishedAt).toLocaleDateString(locale === 'tr' ? 'tr-TR' : locale === 'ru' ? 'ru-RU' : 'en-US') : ''}
</span>
{post.tags.length > 0 && (
<span className="flex items-center gap-1.5">
<Tag className="w-3.5 h-3.5" />
{post.tags[0]}
</span>
)}
</div>
<h2 className="font-heading font-extrabold text-lg text-pine hover:text-turquoise transition duration-150 lowercase leading-snug">
<Link href={`/blog/${post.slug}`}>
{title}
</Link>
</h2>
<p className="text-ink/75 text-xs sm:text-sm leading-relaxed">
{snippet}
</p>
</div>
</div>
<div className="px-6 sm:px-8 pb-6 sm:pb-8 pt-2">
<Link
href={`/blog/${post.slug}`}
className="inline-flex items-center text-xs font-mono font-bold text-turquoise hover:underline uppercase tracking-wider"
>
okumaya devam et
</Link>
</div>
</article>
)
})
)}
</div>
</div>
</div>
)
}