feat: i18n fixes, blog rename, footer cleanup, and contact info update

- Rename /news route to /blog across all pages, links, and sitemap
- Fix hardcoded English text in Experiences, QuoteSection, and Footer
  by wiring all content through the TR/EN dictionaries
- Clean up Footer: remove non-existent page links, add contact column
- Update contact info: new phone numbers, bilgi@ email, No:71 address,
  Instagram @ayrisapart link
- Add suppressHydrationWarning to <body> to silence browser-extension
  attribute mismatch errors
- Add sizes prop to all fill-mode Next.js Image components

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 18:05:30 +03:00
co-authored by Claude Sonnet 4.6
parent 433252a05d
commit 40b4434a7b
34 changed files with 2864 additions and 509 deletions
+88
View File
@@ -0,0 +1,88 @@
'use client'
import { motion } from "framer-motion"
import Image from "next/image"
import Link from "next/link"
export default function NewsClient({ lang, dict, posts }: { lang: string, dict: any, posts: any[] }) {
return (
<main className="bg-[#FAF7F0] min-h-screen">
{/* HEADER */}
<section className="pt-44 pb-20 px-6">
<div className="max-w-4xl mx-auto text-center space-y-10">
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
className="text-7xl md:text-[120px] font-serif text-[#1A1A1A] leading-[0.9] tracking-tight uppercase"
>
{dict.news_page.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-[#1A1A1A]/60 text-lg md:text-xl max-w-2xl mx-auto font-medium leading-relaxed italic"
>
{dict.news_page.excerpt}
</motion.p>
</div>
</section>
{/* POSTS GRID */}
<section className="pb-44 px-6 md:px-12 max-w-[1400px] mx-auto">
{posts.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-20">
{posts.map((post, idx) => (
<motion.div
key={post.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
className="group"
>
<Link href={`/${lang}/blog/${post.slug}`}>
<div className="aspect-[16/10] relative overflow-hidden rounded-[2px] shadow-sm bg-white">
{post.image && (
<Image
src={post.image}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, 33vw"
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
)}
</div>
<div className="mt-8 space-y-4">
<div className="flex items-center space-x-4 text-[11px] font-bold tracking-[0.2em] uppercase text-[#1A1A1A]/40">
<span>{post.author}</span>
<span></span>
<span>{new Date(post.createdAt).toLocaleDateString(lang === 'tr' ? 'tr-TR' : 'en-US')}</span>
</div>
<h2 className="text-3xl font-serif text-[#1A1A1A] group-hover:text-[#C88C4B] transition-colors leading-tight">
{post.title}
</h2>
<p className="text-[#1A1A1A]/60 text-base leading-relaxed line-clamp-2 italic">
{post.excerpt}
</p>
<div className="inline-flex items-center space-x-3 text-[12px] font-bold tracking-[0.3em] uppercase border-b border-[#1A1A1A] pb-1">
<span>{dict.news_page.read}</span>
<span className="text-lg"></span>
</div>
</div>
</Link>
</motion.div>
))}
</div>
) : (
<div className="text-center py-20">
<p className="text-[#1A1A1A]/40 italic">{lang === 'tr' ? 'Henüz yazı bulunmuyor.' : 'No stories found yet.'}</p>
</div>
)}
</section>
</main>
)
}
@@ -0,0 +1,95 @@
'use client'
import { motion } from "framer-motion"
import Image from "next/image"
import Link from "next/link"
import { ArrowLeft, Clock, User } from 'lucide-react'
export default function NewsDetailClient({ lang, dict, post }: { lang: string, dict: any, post: any }) {
return (
<main className="bg-[#FAF7F0] min-h-screen">
{/* HEADER & IMAGE */}
<section className="pt-44 pb-20 px-6 max-w-[1000px] mx-auto">
<div className="space-y-12">
{/* Back Button */}
<Link href={`/${lang}/blog`} className="inline-flex items-center space-x-2 text-[11px] font-bold tracking-[0.4em] uppercase text-[#1A1A1A]/40 hover:text-[#1A1A1A] transition-colors">
<ArrowLeft size={16} />
<span>{lang === 'tr' ? 'Haberlere Dön' : 'Back to News'}</span>
</Link>
<div className="space-y-8 text-center md:text-left">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex flex-wrap justify-center md:justify-start items-center gap-6 text-[12px] font-bold tracking-[0.2em] uppercase text-[#1A1A1A]/30"
>
<div className="flex items-center space-x-2">
<User size={14} className="text-[#C88C4B]" />
<span>{post.author}</span>
</div>
<div className="flex items-center space-x-2">
<Clock size={14} className="text-[#C88C4B]" />
<span>{new Date(post.createdAt).toLocaleDateString(lang === 'tr' ? 'tr-TR' : 'en-US')}</span>
</div>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="text-5xl md:text-[80px] font-serif text-[#1A1A1A] leading-[1.1] tracking-tight"
>
{post.title}
</motion.h1>
</div>
{post.image && (
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1, delay: 0.2 }}
className="aspect-[21/10] relative overflow-hidden rounded-[2px] shadow-2xl"
>
<Image
src={post.image}
alt={post.title}
fill
sizes="100vw"
className="object-cover"
priority
/>
</motion.div>
)}
</div>
</section>
{/* CONTENT */}
<section className="pb-44 px-6 max-w-[800px] mx-auto">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="prose prose-neutral prose-xl max-w-none"
>
<p className="text-2xl md:text-3xl font-serif text-[#1A1A1A] italic leading-relaxed mb-12 opacity-80 border-l-4 border-[#C88C4B] pl-8">
{post.excerpt}
</p>
<div className="text-[#1A1A1A]/80 leading-loose text-lg md:text-xl space-y-8 whitespace-pre-wrap">
{post.content}
</div>
</motion.div>
<div className="mt-32 pt-12 border-t border-[#1A1A1A]/10 text-center">
<Link
href={`/${lang}/contact`}
className="inline-flex items-center space-x-4 text-[13px] font-bold tracking-[0.4em] uppercase border-b-2 border-[#1A1A1A] pb-2 group"
>
<span>{dict.footer.book}</span>
<span className="transform group-hover:translate-x-1 transition-transform"></span>
</Link>
</div>
</section>
</main>
)
}
@@ -1,14 +1,20 @@
import { getDictionary } from "@/dictionaries/get-dictionary"
import NewsDetailClient from "./NewsDetailClient"
import { prisma } from "@/lib/prisma"
import { notFound } from "next/navigation"
export async function generateMetadata({ params }: { params: Promise<{ lang: string, slug: string }> }) {
const { lang, slug } = await params
const dict = await getDictionary(lang as 'en' | 'tr')
// Dynamic title based on slug if possible, or just section title
const post = await prisma.post.findUnique({
where: { slug }
})
if (!post) return { title: 'Post Not Found' }
return {
title: `News - Ayris Apart`,
description: dict.news_page.excerpt,
title: `${post.title} - Ayris Apart`,
description: post.excerpt,
}
}
@@ -16,5 +22,11 @@ export default async function NewsDetailPage({ params }: { params: Promise<{ lan
const { lang, slug } = await params
const dict = await getDictionary(lang as 'en' | 'tr')
return <NewsDetailClient lang={lang} slug={slug} dict={dict} />
const post = await prisma.post.findUnique({
where: { slug }
})
if (!post) notFound()
return <NewsDetailClient lang={lang} dict={dict} post={post} />
}
@@ -1,5 +1,6 @@
import { getDictionary } from "@/dictionaries/get-dictionary"
import NewsClient from "./NewsClient"
import { prisma } from "@/lib/prisma"
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params
@@ -14,5 +15,16 @@ export default async function NewsPage({ params }: { params: Promise<{ lang: str
const { lang } = await params
const dict = await getDictionary(lang as 'en' | 'tr')
return <NewsClient lang={lang} dict={dict} />
// Fetch posts from database based on the selected language
const posts = await prisma.post.findMany({
where: {
lang: lang,
published: true
},
orderBy: {
createdAt: 'desc'
}
})
return <NewsClient lang={lang} dict={dict} posts={posts} />
}
+6 -5
View File
@@ -70,9 +70,10 @@ export default function ContactClient({ lang, dict }: { lang: string, dict: any
<h3 className="text-3xl font-serif">{dict.contact.call.t}</h3>
<div className="space-y-2">
<p className="text-2xl font-medium">+90 543 231 87 13</p>
<p className="text-2xl font-medium">+90 554 376 51 03</p>
<p className="text-lg text-[#1A1A1A]/50 italic">{dict.contact.call.d}</p>
</div>
<Link href="tel:+905432318713" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<Link href="tel:+905543765103" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<span>{dict.contact.btn.call}</span>
<ArrowUpRight size={14} />
</Link>
@@ -91,10 +92,10 @@ export default function ContactClient({ lang, dict }: { lang: string, dict: any
<div className="space-y-6">
<h3 className="text-3xl font-serif">{dict.contact.write.t}</h3>
<div className="space-y-2">
<p className="text-2xl font-medium">hello@ayrisapart.com</p>
<p className="text-2xl font-medium">bilgi@ayrisapart.com</p>
<p className="text-lg text-[#1A1A1A]/50 italic">{dict.contact.write.d}</p>
</div>
<Link href="mailto:hello@ayrisapart.com" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<Link href="mailto:bilgi@ayrisapart.com" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<span>{dict.contact.btn.mail}</span>
<ArrowUpRight size={14} />
</Link>
@@ -114,7 +115,7 @@ export default function ContactClient({ lang, dict }: { lang: string, dict: any
<p className="text-xl text-[#1A1A1A]/60 italic leading-relaxed">
{dict.contact.wa.d}
</p>
<Link href="https://wa.me/905432318713" target="_blank" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<Link href="https://wa.me/905543765103" target="_blank" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<span>{dict.contact.btn.wa}</span>
<ArrowUpRight size={14} />
</Link>
@@ -135,7 +136,7 @@ export default function ContactClient({ lang, dict }: { lang: string, dict: any
<p className="text-xl text-[#1A1A1A]/60 italic leading-relaxed">
{dict.contact.ig.d}
</p>
<Link href="#" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<Link href="https://instagram.com/ayrisapart" target="_blank" className="inline-flex items-center space-x-2 text-xs font-bold uppercase tracking-[0.3em] border-b border-[#1A1A1A] pb-1">
<span>{dict.contact.btn.ig}</span>
<ArrowUpRight size={14} />
</Link>
+38 -1
View File
@@ -37,9 +37,46 @@ export default async function RootLayout({
const { lang } = await params;
const dict = await getDictionary(lang as 'en' | 'tr');
const jsonLd = {
"@context": "https://schema.org",
"@type": "Hotel",
"name": "Ayris Apart",
"description": dict.hero.desc,
"address": {
"@type": "PostalAddress",
"streetAddress": "Orta İskele Caddesi No:71",
"addressLocality": "Milas",
"addressRegion": "Muğla",
"postalCode": "48220",
"addressCountry": "TR"
},
"geo": {
"@type": "GeoCoordinates",
"latitude": 37.037,
"longitude": 27.962
},
"url": "https://ayrisapart.com",
"telephone": "+90",
"image": "https://ayrisapart.com/logo.png",
"amenityFeature": [
{ "@type": "LocationFeatureSpecification", "name": "Free Wi-Fi", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Kitchen", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Air Conditioning", "value": true },
{ "@type": "LocationFeatureSpecification", "name": "Free Parking", "value": true }
],
"checkinTime": "14:00",
"checkoutTime": "11:00"
};
return (
<html lang={lang} className={`${oranienbaum.variable} ${inter.variable} h-full antialiased`}>
<body className="font-sans min-h-full flex flex-col bg-[#FAF7F0] text-[#1A1A1A]">
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
</head>
<body className="font-sans min-h-full flex flex-col bg-[#FAF7F0] text-[#1A1A1A]" suppressHydrationWarning>
<Navbar lang={lang} dict={dict} />
{children}
<Footer lang={lang} dict={dict} />
-120
View File
@@ -1,120 +0,0 @@
'use client'
import { motion } from "framer-motion"
import Image from "next/image"
import Link from "next/link"
import { Calendar, ArrowRight } from 'lucide-react'
export default function NewsClient({ lang, dict }: { lang: string, dict: any }) {
const newsItems = [
{
id: 'hidden-gems-oren',
title: dict.news_page.list.n1.title,
excerpt: dict.news_page.list.n1.excerpt,
image: 'https://images.unsplash.com/photo-1544124499-58912cbddaad?q=80&w=2127&auto=format&fit=crop',
date: 'April 15, 2026',
author: dict.news_page.list.n1.author
},
{
id: 'summer-cocktails-retreat',
title: dict.news_page.list.n2.title,
excerpt: dict.news_page.list.n2.excerpt,
image: 'https://images.unsplash.com/photo-1519046904884-53103b34b206?q=80&w=2073&auto=format&fit=crop',
date: 'April 10, 2026',
author: dict.news_page.list.n2.author
},
{
id: 'luxury-interior-trends',
title: dict.news_page.list.n3.title,
excerpt: dict.news_page.list.n3.excerpt,
image: 'https://images.unsplash.com/photo-1618773928121-c32242e63f39?q=80&w=1964&auto=format&fit=crop',
date: 'April 05, 2026',
author: dict.news_page.list.n3.author
}
]
return (
<main className="bg-[#FAF7F0] min-h-screen">
{/* HEADER SECTION */}
<section className="pt-44 pb-24 px-6">
<div className="max-w-5xl mx-auto text-center space-y-10">
<motion.h1
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1 }}
className="text-7xl md:text-[120px] font-serif text-[#1A1A1A] leading-[0.9] tracking-tight uppercase"
>
{dict.news_page.title}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="text-[#1A1A1A]/70 text-lg md:text-xl max-w-3xl mx-auto font-medium leading-relaxed italic"
>
{dict.news_page.subtitle}
</motion.p>
</div>
</section>
{/* ARTICLES GRID SECTION */}
<section className="pb-44 px-6 md:px-12 max-w-[1400px] mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
{newsItems.map((article, idx) => (
<motion.div
key={article.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: idx * 0.15 }}
className="group cursor-pointer"
>
<Link href={`/${lang}/news/${article.id}`}>
<div className="space-y-8">
{/* Image Card */}
<div className="aspect-[4/5] relative overflow-hidden rounded-[2px] shadow-sm">
<Image
src={article.image}
alt={article.title}
fill
className="object-cover transition-transform duration-1000 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-black/5 group-hover:bg-transparent transition-colors duration-500" />
</div>
{/* Metadata and Title */}
<div className="space-y-4">
<div className="flex items-center space-x-6 text-[11px] font-bold tracking-[0.3em] uppercase text-[#1A1A1A]/40 mb-2">
<span className="flex items-center space-x-2">
<Calendar size={14} className="opacity-50" />
<span>{article.date}</span>
</span>
</div>
<h2 className="text-3xl md:text-4xl font-serif text-[#1A1A1A] leading-tight group-hover:text-[#C88C4B] transition-colors duration-300">
{article.title}
</h2>
<p className="text-[#1A1A1A]/60 text-lg leading-relaxed italic line-clamp-2">
{article.excerpt}
</p>
<div className="inline-flex items-center space-x-2 text-[12px] font-bold tracking-widest uppercase text-[#1A1A1A] pt-2 border-b-2 border-transparent group-hover:border-[#C88C4B] transition-all pb-1">
<span>{dict.news_page.read}</span>
<ArrowRight size={16} className="transform group-hover:translate-x-1 transition-transform" />
</div>
</div>
</div>
</Link>
</motion.div>
))}
</div>
</section>
</main>
)
}
-157
View File
@@ -1,157 +0,0 @@
'use client'
import { motion } from "framer-motion"
import Image from "next/image"
import Link from "next/link"
import { Calendar, User, Share2, ArrowLeft } from 'lucide-react'
export default function NewsDetailClient({ lang, slug, dict }: { lang: string, slug: string, dict: any }) {
// Use dictionary instead of hardcoded data where possible
// For demo, we still use a local map but referring to Ören
const newsData: Record<string, any> = {
'hidden-gems-oren': {
title: dict.news_page.list.n1.title,
date: 'April 15, 2026',
author: dict.news_page.list.n1.author,
category: 'Travel Guide',
image: 'https://images.unsplash.com/photo-1544124499-58912cbddaad?q=80&w=2127&auto=format&fit=crop',
content: [
{ type: 'paragraph', text: dict.news_page.list.n1.excerpt + ' ' + (lang === 'tr' ? 'Örenin saklı koyları, zamanın ötesinde bir huzur sunuyor. Turkuaz suların üzerinden sabah sisi kalkarken, standart haritaların ötesine bakmaya istekli olanlar için gizli sığınaklar kendilerini göstermeye başlıyor. Bugünkü yolculuğumuz bizi Egenin saklı kalbinde bir keşfe çıkarıyor.' : 'Beyond the crowded beaches, the secret coves of Oren offer a peace beyond time. As the morning mist lifts from the turquoise waters, hidden havens begin to reveal themselves to those willing to look beyond standard maps. Our journey today takes us on an exploration in the hidden heart of the Aegean.') },
{ type: 'quote', text: lang === 'tr' ? 'Gerçek keşif yolculuğu yeni manzaralar aramak değil, yeni gözlere sahip olmaktan geçer.' : 'The real voyage of discovery consists not in seeking new landscapes, but in having new eyes.' },
{ type: 'paragraph', text: lang === 'tr' ? 'Gemile koyunun sessiz kıyılarında, zeytinlikler arasında sessizce oturan antik kalıntılar bunlardan sadece biri. Bu sessiz taşların arasında yürürken, Bizanslı tüccarların yankılarını neredeyse duyabilirsiniz. Burası, tarihin, doğanın ve sessiz lüksün harmanlandığı, Ayris Apart\'ın gerçek özünü bulduğumuz yerdir.' : 'The ancient ruins sitting quietly amidst olive groves on the silent shores of Gemile Bay are just one of them. Walking among these silent stones, you can almost hear the echoes of Byzantine merchants. This is where we find the true essence of Ayris Apart, where history, nature, and silent luxury blend.' },
{ type: 'image', url: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?q=80&w=2073&auto=format&fit=crop' }
]
},
'summer-cocktails-retreat': {
title: dict.news_page.list.n2.title,
date: 'April 10, 2026',
author: dict.news_page.list.n2.author,
category: 'Lifestyle',
image: 'https://images.unsplash.com/photo-1519046904884-53103b34b206?q=80&w=2073&auto=format&fit=crop',
content: [
{ type: 'paragraph', text: dict.news_page.list.n2.excerpt }
]
},
'luxury-interior-trends': {
title: dict.news_page.list.n3.title,
date: 'April 05, 2026',
author: dict.news_page.list.n3.author,
category: 'Design',
image: 'https://images.unsplash.com/photo-1618773928121-c32242e63f39?q=80&w=1964&auto=format&fit=crop',
content: [
{ type: 'paragraph', text: dict.news_page.list.n3.excerpt }
]
}
}
const post = newsData[slug] || newsData['hidden-gems-oren']
return (
<main className="bg-[#FAF7F0] min-h-screen">
{/* HEADER SECTION */}
<section className="pt-44 pb-20 px-6">
<div className="max-w-4xl mx-auto text-center space-y-8">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
className="flex items-center justify-center space-x-6 text-[11px] font-bold tracking-[0.4em] uppercase text-[#1A1A1A]/40"
>
<span>{post.category}</span>
<span className="w-1 h-1 bg-[#C88C4B] rounded-full" />
<span>{post.date}</span>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-5xl md:text-7xl lg:text-[88px] font-serif text-[#1A1A1A] leading-[1.1] tracking-tight uppercase"
>
{post.title}
</motion.h1>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="flex items-center justify-center space-x-4 pt-4"
>
<div className="w-12 h-12 rounded-full bg-[#1A1A1A]/5 flex items-center justify-center text-[#1A1A1A]/40 overflow-hidden">
<User size={24} />
</div>
<div className="text-left leading-tight">
<p className="text-[11px] font-bold tracking-widest uppercase text-[#1A1A1A]/30 mb-1">Written By</p>
<p className="text-sm font-medium text-[#1A1A1A]">{post.author}</p>
</div>
</motion.div>
</div>
</section>
{/* FEATURE IMAGE */}
<section className="px-6 md:px-12 max-w-[1400px] mx-auto overflow-hidden">
<motion.div
initial={{ opacity: 0, scale: 1.05 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1.5 }}
className="aspect-[21/9] relative rounded-[2px]"
>
<Image src={post.image} alt={post.title} fill className="object-cover" />
</motion.div>
</section>
{/* CONTENT SECTION */}
<section className="py-24 px-6">
<div className="max-w-2xl mx-auto space-y-12">
{post.content.map((block: any, idx: number) => {
if (block.type === 'paragraph') {
return (
<p key={idx} className={`text-[#1A1A1A]/80 text-xl leading-relaxed italic ${idx === 0 ? 'first-letter:text-7xl first-letter:font-serif first-letter:mr-3 first-letter:float-left first-letter:leading-[0.8] first-letter:mt-1' : ''}`}>
{block.text}
</p>
)
}
if (block.type === 'quote') {
return (
<blockquote key={idx} className="border-l-4 border-[#C88C4B] pl-8 py-4 my-16">
<p className="text-3xl font-serif text-[#1A1A1A] leading-snug">
&ldquo;{block.text}&rdquo;
</p>
</blockquote>
)
}
if (block.type === 'image') {
return (
<div key={idx} className="my-16 -mx-6 md:-mx-24 aspect-video relative overflow-hidden rounded-[2px] shadow-2xl">
<Image src={block.url} alt="Article imagery" fill className="object-cover" />
</div>
)
}
return null
})}
{/* SHARE & BACK */}
<div className="pt-16 border-t border-[#1A1A1A]/10 flex items-center justify-between">
<Link
href={`/${lang}/news`}
className="inline-flex items-center space-x-2 text-[12px] font-bold tracking-widest uppercase text-[#1A1A1A]/40 hover:text-[#1A1A1A] transition-colors"
>
<ArrowLeft size={16} />
<span>{lang === 'tr' ? 'Haberlere Dön' : 'Back to News'}</span>
</Link>
<div className="flex items-center space-x-4">
<span className="text-[11px] font-bold tracking-widest uppercase text-[#1A1A1A]/40">Share</span>
<button className="w-10 h-10 rounded-full border border-[#1A1A1A]/10 flex items-center justify-center hover:bg-[#1A1A1A] hover:text-white transition-all">
<Share2 size={16} />
</button>
</div>
</div>
</div>
</section>
</main>
)
}
+2 -2
View File
@@ -28,9 +28,9 @@ export default async function Home({ params }: { params: Promise<{ lang: string
<Hero lang={lang} dict={dict} />
<Welcome lang={lang} dict={dict} />
<ShowcaseImage />
<QuoteSection />
<QuoteSection dict={dict} />
<SuitesHighlights lang={lang} dict={dict} />
<Experiences />
<Experiences dict={dict} />
<ScrollReveal />
<CallToAction lang={lang} dict={dict} />
+17 -11
View File
@@ -8,21 +8,21 @@ import Amenities from "@/components/Amenities"
export default function SuitesClient({ lang, dict }: { lang: string, dict: any }) {
const suites = [
{ id: 'iris', number: '01', name: dict.suites_page.list.s1.name, desc: dict.suites_page.list.s1.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606641/ayrisapart/Daire%201/photo_1_2024-04-05_12-32-09.jpg', bed: dict.suites_page.list.s1.bed, guests: dict.suites_page.list.s1.guests },
{ id: 'electra', number: '02', name: dict.suites_page.list.s2.name, desc: dict.suites_page.list.s2.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606648/ayrisapart/Daire%202/photo_1_2024-04-05_16-04-34.jpg', bed: dict.suites_page.list.s2.bed, guests: dict.suites_page.list.s2.guests },
{ id: 'arke', number: '03', name: dict.suites_page.list.s3.name, desc: dict.suites_page.list.s3.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606654/ayrisapart/Daire%203/photo_1_2024-04-05_16-06-09.jpg', bed: dict.suites_page.list.s3.bed, guests: dict.suites_page.list.s3.guests },
{ id: 'harpy', number: '04', name: dict.suites_page.list.s4.name, desc: dict.suites_page.list.s4.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606661/ayrisapart/Daire%204/photo_1_2024-04-05_16-07-01.jpg', bed: dict.suites_page.list.s4.bed, guests: dict.suites_page.list.s4.guests },
{ id: 'hydaspes', number: '05', name: dict.suites_page.list.s5.name, desc: dict.suites_page.list.s5.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606671/ayrisapart/Daire%205/photo_1_2024-05-04_15-32-44.jpg', bed: dict.suites_page.list.s5.bed, guests: dict.suites_page.list.s5.guests },
{ id: 'zephyrus', number: '06', name: dict.suites_page.list.s6.name, desc: dict.suites_page.list.s6.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606681/ayrisapart/Daire%206/photo_1_2024-05-04_15-32-44.jpg', bed: dict.suites_page.list.s6.bed, guests: dict.suites_page.list.s6.guests },
{ id: 'pothos', number: '07', name: dict.suites_page.list.s7.name, desc: dict.suites_page.list.s7.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606689/ayrisapart/Daire%207/photo_1_2024-05-04_15-33-34.jpg', bed: dict.suites_page.list.s7.bed, guests: dict.suites_page.list.s7.guests },
{ id: 'thaumas', number: '08', name: dict.suites_page.list.s8.name, desc: dict.suites_page.list.s8.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606696/ayrisapart/Daire%208/photo_1_2024-05-04_15-33-34.jpg', bed: dict.suites_page.list.s8.bed, guests: dict.suites_page.list.s8.guests },
{ id: 'iris', number: '01', name: dict.suites_page.list.s1.name, desc: dict.suites_page.list.s1.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606641/ayrisapart/Daire%201/photo_1_2024-04-05_12-32-09.jpg', bed: dict.suites_page.list.s1.bed, guests: dict.suites_page.list.s1.guests, price: dict.suites_page.list.s1.price },
{ id: 'electra', number: '02', name: dict.suites_page.list.s2.name, desc: dict.suites_page.list.s2.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606648/ayrisapart/Daire%202/photo_4_2024-04-05_16-04-34.jpg', bed: dict.suites_page.list.s2.bed, guests: dict.suites_page.list.s2.guests, price: dict.suites_page.list.s2.price },
{ id: 'arke', number: '03', name: dict.suites_page.list.s3.name, desc: dict.suites_page.list.s3.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606654/ayrisapart/Daire%203/photo_8_2024-04-05_16-06-09.jpg', bed: dict.suites_page.list.s3.bed, guests: dict.suites_page.list.s3.guests, price: dict.suites_page.list.s3.price },
{ id: 'harpy', number: '04', name: dict.suites_page.list.s4.name, desc: dict.suites_page.list.s4.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606661/ayrisapart/Daire%204/photo_8_2024-04-05_16-07-01.jpg', bed: dict.suites_page.list.s4.bed, guests: dict.suites_page.list.s4.guests, price: dict.suites_page.list.s4.price },
{ id: 'hydaspes', number: '05', name: dict.suites_page.list.s5.name, desc: dict.suites_page.list.s5.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606671/ayrisapart/Daire%205/photo_1_2024-05-04_15-32-44.jpg', bed: dict.suites_page.list.s5.bed, guests: dict.suites_page.list.s5.guests, price: dict.suites_page.list.s5.price },
{ id: 'zephyrus', number: '06', name: dict.suites_page.list.s6.name, desc: dict.suites_page.list.s6.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606681/ayrisapart/Daire%206/photo_1_2024-05-04_15-33-08.jpg', bed: dict.suites_page.list.s6.bed, guests: dict.suites_page.list.s6.guests, price: dict.suites_page.list.s6.price },
{ id: 'pothos', number: '07', name: dict.suites_page.list.s7.name, desc: dict.suites_page.list.s7.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606689/ayrisapart/Daire%207/photo_5_2024-05-04_15-33-34.jpg', bed: dict.suites_page.list.s7.bed, guests: dict.suites_page.list.s7.guests, price: dict.suites_page.list.s7.price },
{ id: 'thaumas', number: '08', name: dict.suites_page.list.s8.name, desc: dict.suites_page.list.s8.desc, image: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606696/ayrisapart/Daire%208/photo_9_2024-05-04_15-33-34.jpg', bed: dict.suites_page.list.s8.bed, guests: dict.suites_page.list.s8.guests, price: dict.suites_page.list.s8.price },
]
return (
<main className="bg-[#FAF7F0] min-h-screen z-60 ">
<main className="bg-[#FAF7F0] min-h-screen">
{/* HEADER SECTION */}
<section className="pt-44 pb-20 px-6 ">
<section className="pt-44 pb-20 px-6">
<div className="max-w-4xl mx-auto text-center space-y-10">
<motion.h1
initial={{ opacity: 0, y: 30 }}
@@ -45,7 +45,7 @@ export default function SuitesClient({ lang, dict }: { lang: string, dict: any }
{/* SUITES LIST SECTION */}
<section className="pb-32 px-6 md:px-12 max-w-[1400px] mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-24">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-32">
{suites.map((suite, idx) => (
<motion.div
key={suite.id}
@@ -61,8 +61,14 @@ export default function SuitesClient({ lang, dict }: { lang: string, dict: any }
src={suite.image}
alt={suite.name}
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover transition-transform duration-1000 group-hover:scale-105"
/>
{/* Price Badge */}
<div className="absolute top-6 right-6 bg-white/90 backdrop-blur-md px-6 py-3 rounded-full shadow-xl">
<span className="text-[#1A1A1A] font-serif text-xl">{suite.price}</span>
<span className="text-[#1A1A1A]/40 text-xs ml-1 uppercase tracking-widest">/ {lang === 'tr' ? 'Gece' : 'Night'}</span>
</div>
</div>
{/* Info */}
+54 -37
View File
@@ -7,12 +7,13 @@ import { Users, BedDouble, ArrowLeft } from 'lucide-react'
import Amenities from "@/components/Amenities"
export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id: string, dict: any }) {
// Mapping the 8 mythological suites with their Cloudinary assets
// Mapping the 8 mythological suites with their Cloudinary assets and dictionary prices
const suitesData: Record<string, any> = {
'iris': {
number: '01',
name: dict.suites_page.list.s1.name,
description: dict.suites_page.list.s1.desc,
price: dict.suites_page.list.s1.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606641/ayrisapart/Daire%201/photo_1_2024-04-05_12-32-09.jpg',
guests: dict.suites_page.list.s1.guests,
bed: dict.suites_page.list.s1.bed,
@@ -26,7 +27,8 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
number: '02',
name: dict.suites_page.list.s2.name,
description: dict.suites_page.list.s2.desc,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606648/ayrisapart/Daire%202/photo_1_2024-04-05_16-04-34.jpg',
price: dict.suites_page.list.s2.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606648/ayrisapart/Daire%202/photo_4_2024-04-05_16-04-34.jpg',
guests: dict.suites_page.list.s2.guests,
bed: dict.suites_page.list.s2.bed,
gallery: [
@@ -39,6 +41,7 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
number: '03',
name: dict.suites_page.list.s3.name,
description: dict.suites_page.list.s3.desc,
price: dict.suites_page.list.s3.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606654/ayrisapart/Daire%203/photo_1_2024-04-05_16-06-09.jpg',
guests: dict.suites_page.list.s3.guests,
bed: dict.suites_page.list.s3.bed,
@@ -52,6 +55,7 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
number: '04',
name: dict.suites_page.list.s4.name,
description: dict.suites_page.list.s4.desc,
price: dict.suites_page.list.s4.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606661/ayrisapart/Daire%204/photo_1_2024-04-05_16-07-01.jpg',
guests: dict.suites_page.list.s4.guests,
bed: dict.suites_page.list.s4.bed,
@@ -65,6 +69,7 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
number: '05',
name: dict.suites_page.list.s5.name,
description: dict.suites_page.list.s5.desc,
price: dict.suites_page.list.s5.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606671/ayrisapart/Daire%205/photo_1_2024-05-04_15-32-44.jpg',
guests: dict.suites_page.list.s5.guests,
bed: dict.suites_page.list.s5.bed,
@@ -78,6 +83,7 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
number: '06',
name: dict.suites_page.list.s6.name,
description: dict.suites_page.list.s6.desc,
price: dict.suites_page.list.s6.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606681/ayrisapart/Daire%206/photo_1_2024-05-04_15-32-44.jpg',
guests: dict.suites_page.list.s6.guests,
bed: dict.suites_page.list.s6.bed,
@@ -91,6 +97,7 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
number: '07',
name: dict.suites_page.list.s7.name,
description: dict.suites_page.list.s7.desc,
price: dict.suites_page.list.s7.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606689/ayrisapart/Daire%207/photo_1_2024-05-04_15-33-34.jpg',
guests: dict.suites_page.list.s7.guests,
bed: dict.suites_page.list.s7.bed,
@@ -104,6 +111,7 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
number: '08',
name: dict.suites_page.list.s8.name,
description: dict.suites_page.list.s8.desc,
price: dict.suites_page.list.s8.price,
mainImage: 'https://res.cloudinary.com/du7xohbct/image/upload/v1776606696/ayrisapart/Daire%208/photo_1_2024-05-04_15-33-34.jpg',
guests: dict.suites_page.list.s8.guests,
bed: dict.suites_page.list.s8.bed,
@@ -131,7 +139,7 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
{/* Title and Brief Header */}
<div className="space-y-8">
<motion.h1
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-6xl md:text-[88px] font-serif text-[#1A1A1A] tracking-tight leading-none uppercase"
@@ -153,19 +161,20 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
</div>
{/* Main Visual */}
<motion.div
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 1 }}
className="aspect-[21/9] relative overflow-hidden rounded-[2px] shadow-2xl"
>
<Image
src={suite.mainImage}
alt={suite.name}
fill
className="object-cover"
priority
/>
<Image
src={suite.mainImage}
alt={suite.name}
fill
sizes="100vw"
className="object-cover"
priority
/>
</motion.div>
</div>
</section>
@@ -174,45 +183,53 @@ export default function SuiteDetailClient({ lang, id, dict }: { lang: string, id
<section className="py-24 px-6 md:px-16 max-w-[1400px] mx-auto">
<div className="grid grid-cols-1 md:grid-cols-12 gap-16 border-b border-[#1A1A1A]/10 pb-20">
<div className="md:col-span-4">
<h2 className="text-4xl font-serif text-[#1A1A1A] uppercase tracking-tight">{lang === 'tr' ? 'Suit Hakkında' : 'About Suite'}</h2>
<h2 className="text-4xl font-serif text-[#1A1A1A] uppercase tracking-tight">{lang === 'tr' ? 'Suit Hakkında' : 'About Suite'}</h2>
</div>
<div className="md:col-span-8 space-y-12">
<p className="text-[#1A1A1A]/70 text-2xl leading-relaxed italic max-w-2xl">
{suite.description}
</p>
<Link
href={`/${lang}/reservation`}
className="inline-flex items-center space-x-3 text-[14px] font-bold tracking-[0.4em] uppercase border-b-2 border-[#1A1A1A] pb-2 group"
>
<span className="transform group-hover:translate-x-1 transition-transform">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="rotate-45">
<line x1="12" y1="19" x2="12" y2="5" />
<polyline points="5 12 12 5 19 12" />
</svg>
</span>
<span>{dict.footer.book}</span>
</Link>
<div className="flex justify-between items-baseline">
<span className="text-[13px] font-bold tracking-[0.3em] uppercase text-[#1A1A1A]/40">{lang === 'tr' ? 'Başlayan fiyatlar' : 'Starting at'}</span>
<div className="text-right">
<span className="text-6xl md:text-8xl font-serif text-[#1A1A1A]">{suite.price}</span>
<span className="text-[#1A1A1A]/40 text-lg font-medium ml-2 uppercase tracking-widest">/ {lang === 'tr' ? 'Gece' : 'Night'}</span>
</div>
</div>
<p className="text-[#1A1A1A]/70 text-2xl leading-relaxed italic max-w-2xl">
{suite.description}
</p>
<Link
href={`/${lang}/reservation`}
className="inline-flex items-center space-x-3 text-[14px] font-bold tracking-[0.4em] uppercase border-b-2 border-[#1A1A1A] pb-2 group"
>
<span className="transform group-hover:translate-x-1 transition-transform">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="rotate-45">
<line x1="12" y1="19" x2="12" y2="5" />
<polyline points="5 12 12 5 19 12" />
</svg>
</span>
<span>{dict.footer.book}</span>
</Link>
</div>
</div>
{/* INTEGRATED AMENITIES COMPONENT */}
<div className="mt-20">
<Amenities dict={dict} />
<Amenities dict={dict} />
</div>
</section>
{/* SECTION 3: GALLERY GRID */}
<section className="py-24 px-6 md:px-16 max-w-[1400px] mx-auto pb-44">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="aspect-square relative overflow-hidden rounded-[2px] shadow-lg">
<Image src={suite.gallery[0]} alt="Gallery" fill className="object-cover hover:scale-105 transition-transform duration-1000" />
</div>
<div className="aspect-square relative overflow-hidden rounded-[2px] shadow-lg">
<Image src={suite.gallery[1]} alt="Gallery" fill className="object-cover hover:scale-105 transition-transform duration-1000" />
</div>
<div className="md:col-span-2 aspect-[21/9] relative overflow-hidden rounded-[2px] shadow-lg">
<Image src={suite.gallery[2]} alt="Gallery" fill className="object-cover hover:scale-105 transition-transform duration-1000" />
</div>
<div className="aspect-square relative overflow-hidden rounded-[2px] shadow-lg">
<Image src={suite.gallery[0]} alt="Gallery" fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover hover:scale-105 transition-transform duration-1000" />
</div>
<div className="aspect-square relative overflow-hidden rounded-[2px] shadow-lg">
<Image src={suite.gallery[1]} alt="Gallery" fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover hover:scale-105 transition-transform duration-1000" />
</div>
<div className="md:col-span-2 aspect-[21/9] relative overflow-hidden rounded-[2px] shadow-lg">
<Image src={suite.gallery[2]} alt="Gallery" fill sizes="100vw" className="object-cover hover:scale-105 transition-transform duration-1000" />
</div>
</div>
</section>
+59
View File
@@ -0,0 +1,59 @@
import { MetadataRoute } from 'next'
import { prisma } from '@/lib/prisma'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://ayrisapart.com'
const languages = ['tr', 'en']
// 1. Static Routes
const staticRoutes = [
'',
'/about',
'/suites',
'/blog',
'/contact',
]
const staticEntries: MetadataRoute.Sitemap = []
languages.forEach((lang) => {
staticRoutes.forEach((route) => {
staticEntries.push({
url: `${baseUrl}/${lang}${route}`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: route === '' ? 1 : 0.8,
})
})
})
// 2. Dynamic Suite Routes (8 mythological rooms)
const suites = ['iris', 'electra', 'arke', 'harpy', 'hydaspes', 'zephyrus', 'pothos', 'thaumas']
const suiteEntries: MetadataRoute.Sitemap = []
languages.forEach((lang) => {
suites.forEach((id) => {
suiteEntries.push({
url: `${baseUrl}/${lang}/suites/${id}`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.7,
})
})
})
// 3. Dynamic News Routes (from Database)
const posts = await prisma.post.findMany({
where: { published: true },
select: { slug: true, lang: true, updatedAt: true }
})
const newsEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${baseUrl}/${post.lang}/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'monthly',
priority: 0.6,
}))
return [...staticEntries, ...suiteEntries, ...newsEntries]
}