feat: Implement Phase 2 features including blog, collections, saved listings, manifest, and api cron sync
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import ListingCard from '@/components/ListingCard'
|
||||
import { Heart, Share2, ClipboardCheck } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
allListings: any[]
|
||||
locale: string
|
||||
}
|
||||
|
||||
export default function SavedClient({ allListings, locale }: Props) {
|
||||
const searchParams = useSearchParams()
|
||||
const [savedIds, setSavedIds] = useState<string[]>([])
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true)
|
||||
const paramIds = searchParams.get('ids')
|
||||
|
||||
if (paramIds) {
|
||||
// Load shared favorites
|
||||
const ids = paramIds.split(',').filter(Boolean)
|
||||
setSavedIds(ids)
|
||||
// Save shared list to local storage as well
|
||||
localStorage.setItem('savedListingIds', JSON.stringify(ids))
|
||||
} else {
|
||||
// Load personal favorites from localStorage
|
||||
try {
|
||||
const stored = localStorage.getItem('savedListingIds')
|
||||
if (stored) {
|
||||
setSavedIds(JSON.parse(stored))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error reading localStorage:', e)
|
||||
}
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const handleShare = () => {
|
||||
if (savedIds.length === 0) return
|
||||
const shareUrl = `${window.location.origin}${window.location.pathname}?ids=${savedIds.join(',')}`
|
||||
|
||||
navigator.clipboard.writeText(shareUrl).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
})
|
||||
}
|
||||
|
||||
// Filter listings matching saved IDs
|
||||
const filteredListings = allListings.filter(l => savedIds.includes(l.id))
|
||||
|
||||
if (!isClient) {
|
||||
return (
|
||||
<div className="py-20 text-center font-mono text-xs text-shutter animate-pulse">
|
||||
liste yükleniyor...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Title / Action bar */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-dashed border-pine/8 pb-6">
|
||||
<div>
|
||||
<span className="text-[10px] font-mono tracking-widest text-shutter uppercase bg-paper px-3 py-1.5 rounded-full border border-pine/5">
|
||||
kişisel rehberiniz
|
||||
</span>
|
||||
<h1 className="text-3xl font-heading font-extrabold text-pine lowercase mt-2.5">
|
||||
kaydedilen <span className="text-turquoise">mekanlar</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{savedIds.length > 0 && (
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="inline-flex items-center gap-2 bg-turquoise hover:bg-turquoise/90 text-paper text-xs font-bold py-3 px-5 rounded-xl shadow-sm transition active:scale-95 duration-150 shrink-0 self-start"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<ClipboardCheck className="w-4 h-4 text-paper" />
|
||||
Paylaşım Linki Kopyalandı!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Share2 className="w-4 h-4 text-paper" />
|
||||
Listeyi Paylaş (URL Kopyala)
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Grid List */}
|
||||
{filteredListings.length === 0 ? (
|
||||
<div className="bg-paper border border-pine/8 rounded-3xl p-12 text-center space-y-4 max-w-xl mx-auto">
|
||||
<div className="w-12 h-12 rounded-full bg-stone/50 border border-pine/5 flex items-center justify-center mx-auto text-shutter">
|
||||
<Heart className="w-5 h-5 fill-transparent" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-heading font-bold text-pine lowercase text-sm">listeniz henüz boş</h3>
|
||||
<p className="text-ink/65 text-xs mt-1.5 max-w-sm mx-auto leading-relaxed">
|
||||
Mekan kartlarındaki kalp simgesine tıklayarak beğendiğiniz yerleri buraya ekleyebilir ve daha sonra hızlıca erişebilirsiniz.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredListings.map((listing) => (
|
||||
<ListingCard key={listing.id} listing={listing} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Suspense } from 'react'
|
||||
import { mockDb } from '@/lib/mockDb'
|
||||
import SavedClient from './SavedClient'
|
||||
|
||||
export async function generateMetadata() {
|
||||
return {
|
||||
title: 'Kaydedilen Yerler — Marmaris Local',
|
||||
description: 'Marmaris rehberinde kaydettiğiniz ve beğendiğiniz tüm yerel adresler.'
|
||||
}
|
||||
}
|
||||
|
||||
export default async function SavedPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params
|
||||
const allListings = await mockDb.getListings()
|
||||
|
||||
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">
|
||||
<Suspense fallback={<div className="text-xs font-mono py-12 text-center text-shutter">yükleniyor...</div>}>
|
||||
<SavedClient allListings={allListings} locale={locale} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user