feat: complete UI/UX improvements and localization

This commit is contained in:
AyrisAI
2026-07-15 14:42:51 +03:00
parent 2d149f1178
commit a36b833e00
32 changed files with 3667 additions and 348 deletions
+181
View File
@@ -0,0 +1,181 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { Link } from '@/i18n/routing'
import Image from 'next/image'
import { notFound } from 'next/navigation'
import { CheckCircle2, Maximize, Users, BedDouble, ArrowRight } from 'lucide-react'
// Bu sayfa parametre olarak dinamik slug alacak
export default async function RoomDetailPage({ params }: { params: Promise<{ locale: string, slug: string }> }) {
const { locale, slug } = await params
setRequestLocale(locale)
const tRooms = await getTranslations('rooms')
const MOCK_ROOMS = [
{
slug: '1-0-daire',
name: tRooms('room_list.1_0_daire.name'),
description: tRooms('room_list.1_0_daire.desc'),
capacity: 2,
size: '25m²',
beds: '1',
image: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
gallery: [
'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80'
],
amenities: ['Mini Mutfak', 'Klima', 'Balkon', 'WiFi', 'TV', 'Saç Kurutma Makinesi']
},
{
slug: '1-1-daire-a',
name: tRooms('room_list.1_1_daire_a.name'),
description: tRooms('room_list.1_1_daire_a.desc'),
capacity: 2,
size: '40m²',
beds: '1 + Sofa',
image: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
gallery: [
'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80'
],
amenities: ['Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi', 'Oturma Alanı', 'Çamaşır Makinesi']
},
{
slug: '1-1-daire-b',
name: tRooms('room_list.1_1_daire_b.name'),
description: tRooms('room_list.1_1_daire_b.desc'),
capacity: 4,
size: '50m²',
beds: '2',
image: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
gallery: [
'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80'
],
amenities: ['2 Oda', 'Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi', 'Geniş Oturma Alanı']
}
];
const room = MOCK_ROOMS.find(r => r.slug === slug);
if (!room) {
notFound();
}
return (
<div className="bg-background min-h-screen text-on-surface">
{/* Hero Section */}
<section className="relative h-[60vh] md:h-[70vh] w-full">
<Image
src={room.image}
alt={room.name}
fill
sizes="100vw"
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-black/40" />
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<h1 className="font-serif text-[48px] md:text-[64px] text-white font-bold drop-shadow-md">
{room.name}
</h1>
</div>
</div>
</section>
{/* Main Content */}
<section className="max-w-container-max mx-auto px-margin-mobile md:px-gutter py-16">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Left Column - Details */}
<div className="lg:col-span-2 space-y-12">
{/* Badges */}
<div className="flex flex-wrap gap-4">
<div className="flex items-center gap-2 bg-surface-container px-4 py-2 rounded-full text-secondary font-medium">
<Users className="w-5 h-5 text-primary" />
<span>Maks {room.capacity} Kişi</span>
</div>
<div className="flex items-center gap-2 bg-surface-container px-4 py-2 rounded-full text-secondary font-medium">
<Maximize className="w-5 h-5 text-primary" />
<span>{room.size}</span>
</div>
<div className="flex items-center gap-2 bg-surface-container px-4 py-2 rounded-full text-secondary font-medium">
<BedDouble className="w-5 h-5 text-primary" />
<span>{room.beds} Yatak</span>
</div>
</div>
{/* Description */}
<div className="prose prose-lg text-on-surface-variant">
<p className="text-[18px] leading-relaxed">
{room.description}
</p>
</div>
{/* Amenities */}
<div>
<h3 className="font-serif text-[28px] text-secondary font-semibold mb-6">
{tRooms('details.features')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{room.amenities.map((amenity, index) => (
<div key={index} className="flex items-center gap-3">
<CheckCircle2 className="w-5 h-5 text-accent-green flex-shrink-0" />
<span className="text-on-surface-variant">{amenity}</span>
</div>
))}
</div>
</div>
{/* Mini Gallery */}
<div>
<h3 className="font-serif text-[28px] text-secondary font-semibold mb-6">
{tRooms('details.gallery')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{room.gallery.map((img, i) => (
<div key={i} className="relative aspect-square rounded-2xl overflow-hidden shadow-sm hover:shadow-md transition-shadow">
<Image
src={img}
alt={`${room.name} gallery ${i + 1}`}
fill
sizes="(max-width: 640px) 100vw, 33vw"
className="object-cover hover:scale-105 transition-transform duration-500"
/>
</div>
))}
</div>
</div>
</div>
{/* Right Column - Sticky Sidebar */}
<div className="lg:col-span-1">
<div className="sticky top-32 bg-surface-container rounded-[24px] p-8 shadow-sm border border-outline/10">
<h4 className="font-serif text-[24px] text-secondary font-semibold mb-2">
Bilgi Alın
</h4>
<p className="text-on-surface-variant text-[15px] mb-8">
Tarihlerinize uygun fiyat ve müsaitlik durumunu öğrenmek için bizimle iletişime geçin.
</p>
<Link
href="/iletisim"
className="w-full flex items-center justify-center gap-2 bg-primary hover:bg-primary-hover text-white py-4 rounded-full font-bold transition-all duration-300 shadow-md hover:shadow-lg cursor-pointer focus:outline-none focus-visible:ring-4 focus-visible:ring-primary/50 focus-visible:ring-offset-2"
>
{tRooms('details.book_now_cta')}
<ArrowRight className="w-5 h-5" />
</Link>
</div>
</div>
</div>
</section>
</div>
)
}
+107
View File
@@ -0,0 +1,107 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import { Link } from '@/i18n/routing'
import Image from 'next/image'
import { Users, Square, Info } from 'lucide-react'
export default async function RoomsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const tNav = await getTranslations('nav')
const tRooms = await getTranslations('rooms')
const MOCK_ROOMS = [
{
slug: '1-0-daire',
name: tRooms('room_list.1_0_daire.name'),
description: tRooms('room_list.1_0_daire.desc'),
capacity: 2,
size: '25m²',
image: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
amenities: ['Mini Mutfak', 'Klima', 'Balkon', 'WiFi']
},
{
slug: '1-1-daire-a',
name: tRooms('room_list.1_1_daire_a.name'),
description: tRooms('room_list.1_1_daire_a.desc'),
capacity: 2,
size: '40m²',
image: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
amenities: ['Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi', 'Oturma Alanı']
},
{
slug: '1-1-daire-b',
name: tRooms('room_list.1_1_daire_b.name'),
description: tRooms('room_list.1_1_daire_b.desc'),
capacity: 4,
size: '50m²',
image: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
amenities: ['2 Oda', 'Ayrı Mutfak', 'Klima', 'Balkon', 'WiFi']
}
];
return (
<div className="flex-1 bg-gray-50 py-12 pt-32">
<div className="container mx-auto px-4">
<div className="mb-10 text-center max-w-2xl mx-auto">
<h1 className="text-4xl font-bold text-gray-900 mb-4">{tRooms('title')}</h1>
<p className="text-gray-600 text-lg">
{tRooms('description')}
</p>
</div>
<div className="space-y-12 max-w-5xl mx-auto">
{MOCK_ROOMS.map((room) => (
<div key={room.slug} className="bg-white rounded-2xl overflow-hidden border shadow-sm flex flex-col md:flex-row group">
<div className="relative w-full md:w-2/5 h-64 md:h-auto bg-gray-100">
<Image
src={room.image}
alt={room.name}
fill
sizes="(max-width: 768px) 100vw, 40vw"
className="object-cover group-hover:scale-105 transition-transform duration-500"
/>
</div>
<div className="p-6 md:p-8 flex-1 flex flex-col">
<div className="flex justify-between items-start mb-4">
<h2 className="text-2xl font-bold text-gray-900">{room.name}</h2>
</div>
<p className="text-gray-600 mb-6 flex-1">
{room.description}
</p>
<div className="flex flex-wrap gap-4 mb-8">
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Users className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.capacity} {tRooms('capacity')}</span>
</div>
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Square className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.size}</span>
</div>
<div className="flex items-center gap-1.5 text-sm text-gray-500 font-medium">
<Info className="w-4 h-4 text-[#2FB6C4]" />
<span>{room.amenities.length} {tRooms('amenities')}</span>
</div>
</div>
<div className="flex items-center justify-between mt-auto border-t pt-6">
<div className="text-sm text-gray-500">
<span className="font-bold text-gray-900 text-lg">{tRooms('ask_price')}</span> {tRooms('per_night')}
</div>
<Link
href={`/dairelerimiz/${room.slug}`}
className="px-6 py-2.5 bg-[#2FB6C4] hover:bg-[#2597a3] text-white rounded-md font-medium transition-colors duration-300 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-[#2FB6C4] focus-visible:ring-offset-2"
>
{tRooms('details_btn')}
</Link>
</div>
</div>
</div>
))}
</div>
</div>
</div>
)
}
+182
View File
@@ -0,0 +1,182 @@
'use client'
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import Image from 'next/image'
import BougainvilleaMotif from '@/components/BougainvilleaMotif'
import { useTranslations } from 'next-intl'
import { X } from 'lucide-react'
export default function GalleryPage() {
const t = useTranslations('gallery')
const [activeFilter, setActiveFilter] = useState('all')
const [selectedImage, setSelectedImage] = useState<string | null>(null)
const filters = [
{ id: 'all', label: t('filters.all') },
{ id: 'rooms', label: t('filters.rooms') },
{ id: 'exterior', label: t('filters.exterior') },
{ id: 'view', label: t('filters.view') },
{ id: 'balcony', label: t('filters.balcony') },
]
const galleryItems = [
{
id: 1,
category: 'rooms',
title: t('items.1'),
src: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
colSpan: 'col-span-1',
},
{
id: 2,
category: 'exterior',
title: t('items.2'),
src: 'https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?auto=format&fit=crop&q=80',
colSpan: 'col-span-1 md:col-span-2 lg:col-span-1',
},
{
id: 3,
category: 'view',
title: t('items.3'),
src: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80',
colSpan: 'col-span-1',
},
{
id: 4,
category: 'balcony',
title: t('items.4'),
src: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80',
colSpan: 'col-span-1 md:col-span-2',
},
{
id: 5,
category: 'rooms',
title: t('items.5'),
src: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80',
colSpan: 'col-span-1',
},
{
id: 6,
category: 'view',
title: t('items.6'),
src: 'https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80',
colSpan: 'col-span-1 md:col-span-2 lg:col-span-1',
},
]
const filteredItems = galleryItems.filter(item =>
activeFilter === 'all' || item.category === activeFilter
)
return (
<div className="bg-background min-h-screen text-on-surface pt-32 pb-section-gap container mx-auto px-4">
{/* Header Section */}
<header className="relative mb-16 max-w-2xl">
<div className="absolute -top-6 -left-4 opacity-80 -rotate-6 pointer-events-none text-accent-pink">
<BougainvilleaMotif className="w-16 h-16" />
</div>
<h1 className="font-serif text-[40px] md:text-[48px] font-semibold text-secondary mb-4 relative z-10">
{t('title')}
</h1>
<p className="font-sans text-[18px] text-on-surface-variant max-w-xl">
{t('description')}
</p>
</header>
{/* Filter Bar */}
<div className="flex flex-wrap items-center gap-4 mb-12">
{filters.map((filter) => (
<button
key={filter.id}
onClick={() => setActiveFilter(filter.id)}
className={`px-6 py-2 rounded-full font-sans font-bold text-[14px] uppercase tracking-wider transition-all duration-300 border-2 cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
activeFilter === filter.id
? 'border-primary text-primary bg-primary/10'
: 'border-transparent text-secondary hover:border-primary/30'
}`}
>
{filter.label}
</button>
))}
</div>
{/* Masonry-like Grid */}
<motion.div layout className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<AnimatePresence>
{filteredItems.map((item) => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.4 }}
className={`group cursor-pointer focus:outline-none focus-visible:ring-4 focus-visible:ring-primary rounded-[24px] ${item.colSpan}`}
onClick={() => setSelectedImage(item.src)}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setSelectedImage(item.src);
}
}}
>
<div className="relative overflow-hidden rounded-[24px] bg-surface-container shadow-sm hover:shadow-xl transition-all duration-500 aspect-[4/3] md:aspect-auto md:h-80">
<Image
src={item.src}
alt={item.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transform group-hover:scale-105 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-6">
<span className="text-white font-serif text-[24px] font-medium drop-shadow-md">
{item.title}
</span>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</motion.div>
{/* Lightbox Overlay */}
<AnimatePresence>
{selectedImage && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4 md:p-8 backdrop-blur-sm"
onClick={() => setSelectedImage(null)}
>
<button
aria-label="Close lightbox"
className="absolute top-6 right-6 text-white/70 hover:text-white transition-colors duration-300 z-50 p-2 bg-black/20 rounded-full cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
onClick={() => setSelectedImage(null)}
>
<X className="w-8 h-8" />
</button>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="relative w-full max-w-5xl h-[80vh] bg-transparent rounded-lg overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
<Image
src={selectedImage}
alt="Enlarged view"
fill
className="object-contain"
sizes="100vw"
quality={90}
/>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
+196
View File
@@ -0,0 +1,196 @@
'use client'
import { motion } from 'framer-motion'
import Image from 'next/image'
import BougainvilleaMotif from '@/components/BougainvilleaMotif'
import { MapPin, Waves, ShoppingBasket, Coffee, PersonStanding, Phone, Mail } from 'lucide-react'
import { useTranslations } from 'next-intl'
const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1]
export default function ContactPage() {
const t = useTranslations('contact')
return (
<div className="bg-background min-h-screen text-on-surface pt-32 pb-section-gap">
{/* Hero Section */}
<section className="px-margin-mobile md:px-gutter max-w-container-max mx-auto mb-12">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: EASE }}
className="relative inline-block mb-4"
>
<span className="absolute -top-6 -left-4 text-accent-pink/30 -rotate-12 pointer-events-none">
<BougainvilleaMotif className="w-12 h-12" />
</span>
<h1 className="font-serif text-[40px] md:text-[48px] font-semibold text-on-surface">{t('title')}</h1>
</motion.div>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.1, ease: EASE }}
className="font-sans text-[18px] text-on-surface-variant max-w-2xl"
>
{t('description')}
</motion.p>
</section>
{/* Location Grid */}
<section className="bg-surface-container-low py-16 mb-16">
<div className="px-margin-mobile md:px-gutter max-w-container-max mx-auto grid grid-cols-1 lg:grid-cols-12 gap-gutter items-start">
{/* Map Container */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: EASE }}
className="lg:col-span-8 rounded-[24px] overflow-hidden aspect-video shadow-[0_32px_64px_-12px_rgba(112,90,76,0.08)] border border-outline-variant/30 relative bg-surface-container-high"
>
<div className="w-full h-full flex flex-col items-center justify-center text-secondary-container">
<MapPin className="w-16 h-16 mb-4 text-primary/40" />
<span className="text-primary/60 font-sans font-medium">{t('connect.map_placeholder')}</span>
</div>
{/* Mock Map Interaction Layer */}
<div className="absolute bottom-6 left-6 bg-surface/90 backdrop-blur-md p-4 rounded-xl shadow-lg border border-outline-variant/20 flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white">
<MapPin className="w-5 h-5" />
</div>
<div>
<p className="font-sans font-bold text-[12px] text-on-surface uppercase tracking-wider">{t('connect.address_title')}</p>
<p className="font-sans text-[16px] text-secondary">{t('connect.address_value')}</p>
</div>
</div>
</motion.div>
{/* Highlights List */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: EASE, delay: 0.2 }}
className="lg:col-span-4 space-y-8"
>
<div className="relative">
<h2 className="font-serif text-[24px] font-medium text-secondary">{t('walking_distance')}</h2>
<div className="h-1 w-12 bg-accent-pink mt-2 rounded-full"></div>
</div>
<ul className="space-y-6">
{[
{ icon: Waves, title: t('locations.beach.title'), desc: t('locations.beach.desc') },
{ icon: ShoppingBasket, title: t('locations.market.title'), desc: t('locations.market.desc') },
{ icon: Coffee, title: t('locations.cafe.title'), desc: t('locations.cafe.desc') },
{ icon: PersonStanding, title: t('locations.hiking.title'), desc: t('locations.hiking.desc') },
].map((item, idx) => (
<li key={idx} className="flex items-start gap-4 p-4 rounded-2xl bg-surface transition-transform hover:translate-x-2 duration-300 shadow-sm border border-outline-variant/10">
<item.icon className="w-6 h-6 text-primary mt-1 shrink-0" />
<div>
<p className="font-sans font-bold text-[14px] text-on-surface tracking-wide">{item.title}</p>
<p className="font-sans text-[16px] text-on-surface-variant mt-1">{item.desc}</p>
</div>
</li>
))}
</ul>
</motion.div>
</div>
</section>
{/* Contact Section */}
<section className="px-margin-mobile md:px-gutter max-w-container-max mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Contact Form Card */}
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: EASE }}
className="bg-surface p-8 md:p-12 rounded-[32px] shadow-[0_32px_64px_-12px_rgba(112,90,76,0.08)] border border-outline-variant/20"
>
<h3 className="font-serif text-[32px] font-semibold text-on-surface mb-8">{t('form.title')}</h3>
<form className="space-y-6" onSubmit={(e) => e.preventDefault()}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="font-sans font-bold text-secondary text-[12px] uppercase tracking-widest ml-1">{t('form.name')}</label>
<input type="text" placeholder={t('form.name_placeholder')} className="w-full bg-surface-container-low border-none rounded-xl p-4 focus:ring-2 focus:ring-primary focus:outline-none transition-all font-sans text-[16px] outline-none" />
</div>
<div className="space-y-2">
<label className="font-sans font-bold text-secondary text-[12px] uppercase tracking-widest ml-1">{t('form.email')}</label>
<input type="email" placeholder={t('form.email_placeholder')} className="w-full bg-surface-container-low border-none rounded-xl p-4 focus:ring-2 focus:ring-primary focus:outline-none transition-all font-sans text-[16px] outline-none" />
</div>
</div>
<div className="space-y-2">
<label className="font-sans font-bold text-secondary text-[12px] uppercase tracking-widest ml-1">{t('form.subject')}</label>
<input type="text" placeholder={t('form.subject_placeholder')} className="w-full bg-surface-container-low border-none rounded-xl p-4 focus:ring-2 focus:ring-primary focus:outline-none transition-all font-sans text-[16px] outline-none" />
</div>
<div className="space-y-2">
<label className="font-sans font-bold text-secondary text-[12px] uppercase tracking-widest ml-1">{t('form.message')}</label>
<textarea rows={4} placeholder={t('form.message_placeholder')} className="w-full bg-surface-container-low border-none rounded-xl p-4 focus:ring-2 focus:ring-primary focus:outline-none transition-all font-sans text-[16px] outline-none"></textarea>
</div>
<button type="submit" className="w-full bg-accent-pink text-white py-4 rounded-full font-sans font-bold text-[14px] tracking-wider uppercase hover:opacity-90 transition-all duration-300 transform hover:-translate-y-1 shadow-md cursor-pointer focus:outline-none focus-visible:ring-4 focus-visible:ring-accent-pink/50 focus-visible:ring-offset-2">
{t('form.submit')}
</button>
</form>
</motion.div>
{/* Contact Details & Visual */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: EASE, delay: 0.2 }}
className="space-y-12"
>
<div className="space-y-6">
<h3 className="font-serif text-[32px] font-semibold text-on-surface">{t('connect.title')}</h3>
<p className="font-sans text-[18px] text-on-surface-variant">
{t('connect.description')}
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
<Phone className="w-5 h-5" />
</div>
<div>
<p className="font-sans font-bold text-[12px] text-on-surface-variant uppercase tracking-wider">{t('connect.phone')}</p>
<p className="font-sans text-[16px] text-secondary font-medium">+90 530 510 98 42</p>
</div>
</div>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center text-primary shrink-0">
<Mail className="w-5 h-5" />
</div>
<div>
<p className="font-sans font-bold text-[12px] text-on-surface-variant uppercase tracking-wider">{t('connect.email')}</p>
<p className="font-sans text-[16px] text-secondary font-medium">iletisim@sitarapart.com</p>
</div>
</div>
</div>
{/* Decorative Image */}
<div className="relative rounded-[24px] overflow-hidden aspect-[4/3] shadow-md border border-outline-variant/20">
<Image
src="https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?auto=format&fit=crop&q=80"
alt="Akyaka Details"
fill
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover"
/>
</div>
</motion.div>
</div>
</section>
</div>
)
}
+18 -10
View File
@@ -1,24 +1,28 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Literata, Nunito_Sans } from "next/font/google";
import { NextIntlClientProvider } from 'next-intl';
import { getMessages, setRequestLocale } from 'next-intl/server';
import { notFound } from 'next/navigation';
import { routing } from '@/i18n/routing';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
import "../globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
const nunito_sans = Nunito_Sans({
variable: "--font-nunito-sans",
subsets: ["latin"],
weight: ["400", "600", "700"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const literata = Literata({
variable: "--font-literata",
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
});
export const metadata: Metadata = {
title: "Boilerplate App",
description: "Next.js boilerplate with next-intl and NextAuth",
title: "Sitar Apart & Otel",
description: "Akyaka'nın Huzuru Sizinle!",
};
export function generateStaticParams() {
@@ -44,11 +48,15 @@ export default async function RootLayout({
return (
<html
lang={locale}
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${nunito_sans.variable} ${literata.variable} h-full antialiased font-sans`}
>
<body className="min-h-full flex flex-col" suppressHydrationWarning>
<body className="min-h-full flex flex-col bg-[#F8FAFC] text-[#0F172A]" suppressHydrationWarning>
<NextIntlClientProvider messages={messages}>
{children}
<Header />
<main className="flex-1 flex flex-col">
{children}
</main>
<Footer />
</NextIntlClientProvider>
</body>
</html>
+185 -38
View File
@@ -1,48 +1,195 @@
import { getTranslations, setRequestLocale } from 'next-intl/server'
import Link from 'next/link'
'use client'
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations('hero')
const nav = await getTranslations('nav')
const footer = await getTranslations('footer')
import { useTranslations } from 'next-intl'
import { Link } from '@/i18n/routing'
import { motion, useReducedMotion } from 'framer-motion'
import { MapPin, Wifi, UtensilsCrossed, Waves, Car } from 'lucide-react'
import Image from 'next/image'
import BougainvilleaMotif from '@/components/BougainvilleaMotif'
const EASE: [number, number, number, number] = [0.22, 1, 0.36, 1]
export default function HomePage() {
const tHero = useTranslations('hero')
const tFeatures = useTranslations('features')
const tRooms = useTranslations('home_rooms')
const prefersReduced = useReducedMotion()
const heroContainer = prefersReduced ? undefined : {
hidden: {},
show: { transition: { staggerChildren: 0.12 } }
}
const heroItem = prefersReduced ? undefined : {
hidden: { opacity: 0, y: 40, filter: 'blur(8px)' },
show: {
opacity: 1,
y: 0,
filter: 'blur(0px)',
transition: { duration: 0.8, ease: EASE }
}
}
return (
<div className="flex-1 flex flex-col min-h-screen">
<header className="px-6 py-4 border-b flex items-center justify-between">
<div className="font-bold text-xl">Boilerplate</div>
<nav className="space-x-4">
<Link href="/" className="text-gray-600 hover:text-gray-900">{nav('home')}</Link>
<Link href="/about" className="text-gray-600 hover:text-gray-900">{nav('about')}</Link>
<Link href="/contact" className="text-gray-600 hover:text-gray-900">{nav('contact')}</Link>
<Link href="/login" className="px-4 py-2 bg-blue-600 text-white rounded-md text-sm">Login</Link>
</nav>
<div className="bg-background text-on-surface">
{/* Hero Section */}
<header className="relative h-screen w-full flex items-center justify-center overflow-hidden">
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1582719508461-905c673771fd?auto=format&fit=crop&q=80"
alt="Sitar Apart Hotel View"
fill
className="object-cover"
priority
/>
<div className="absolute inset-0 bg-gradient-to-b from-secondary/40 via-secondary/20 to-secondary/60" />
</div>
<motion.div
variants={heroContainer}
initial="hidden"
animate="show"
className="relative z-10 text-center px-margin-mobile max-w-3xl"
>
<motion.h1 variants={heroItem} className="font-serif text-[40px] md:text-[60px] leading-[1.2] font-semibold text-white mb-6 drop-shadow-lg">
{tHero('title')}
</motion.h1>
<motion.p variants={heroItem} className="font-sans text-[18px] text-white/90 mb-10 max-w-xl mx-auto leading-relaxed drop-shadow-md">
{tHero('description')}
</motion.p>
<motion.div variants={heroItem}>
<Link
href="/dairelerimiz"
className="inline-block bg-accent-pink text-white px-10 py-5 rounded-full font-sans font-bold text-[16px] tracking-wider uppercase hover:scale-105 transition-all duration-300 shadow-xl cursor-pointer focus:outline-none focus-visible:ring-4 focus-visible:ring-white/50 focus-visible:ring-offset-2 focus-visible:ring-offset-accent-pink"
>
{tHero('cta')}
</Link>
</motion.div>
</motion.div>
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce text-white">
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</div>
</header>
<main className="flex-1 flex flex-col items-center justify-center text-center px-4">
<h1 className="text-5xl font-extrabold tracking-tight text-gray-900 sm:text-7xl mb-6">
{t('title')}
</h1>
<p className="mt-4 text-xl text-gray-500 max-w-2xl mx-auto mb-8">
This is a dummy landing page to demonstrate the localization setup using next-intl.
</p>
<div className="flex space-x-4">
<button className="px-8 py-3 bg-blue-600 text-white rounded-full font-medium hover:bg-blue-700 transition">
{t('cta')}
</button>
<div className="flex bg-gray-100 rounded-full p-1">
<Link href="/tr" className={`px-4 py-2 rounded-full text-sm font-medium ${locale === 'tr' ? 'bg-white shadow-sm' : 'text-gray-500 hover:text-gray-900'}`}>TR</Link>
<Link href="/en" className={`px-4 py-2 rounded-full text-sm font-medium ${locale === 'en' ? 'bg-white shadow-sm' : 'text-gray-500 hover:text-gray-900'}`}>EN</Link>
</div>
{/* Amenities Strip */}
<section className="bg-surface-container-lowest py-12 relative z-20 -mt-10 mx-margin-mobile md:mx-auto max-w-container-max rounded-[16px] shadow-[0_32px_64px_-12px_rgba(112,90,76,0.04)] border border-outline-variant/20">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 px-gutter">
{[
{ icon: Waves, label: tFeatures('pool') },
{ icon: Wifi, label: tFeatures('wifi') },
{ icon: UtensilsCrossed, label: tFeatures('kitchen') },
{ icon: Car, label: tFeatures('parking') }
].map((amenity, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.5, delay: idx * 0.1, ease: EASE }}
className="flex flex-col items-center text-center gap-3 group"
>
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center text-primary transition-transform duration-300 group-hover:scale-110">
<amenity.icon className="w-6 h-6" />
</div>
<span className="font-sans font-bold text-[14px] text-secondary tracking-wide">{amenity.label}</span>
</motion.div>
))}
</div>
</main>
</section>
<footer className="py-6 border-t text-center text-gray-500 text-sm">
<p>{footer('rights')} <a href="https://ayris.tech" className="hover:text-blue-600 transition">Created by ayris.tech</a></p>
</footer>
{/* Apartments Section */}
<section className="py-section-gap px-margin-mobile max-w-container-max mx-auto overflow-visible">
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.8, ease: EASE }}
className="relative mb-16 text-center"
>
<div className="absolute -top-12 left-1/2 -translate-x-1/2 w-32 h-32 flex items-center justify-center opacity-15 -rotate-6 pointer-events-none text-accent-pink">
<BougainvilleaMotif className="w-24 h-24" />
</div>
<h2 className="font-serif text-[32px] md:text-[40px] font-semibold text-secondary mb-4 relative z-10">
{tRooms('title')}
</h2>
<p className="text-on-surface-variant text-[16px] max-w-xl mx-auto font-sans">
{tRooms('subtitle')}
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-gutter">
{[
{
id: '1-0-daire',
title: tRooms('room_1.title'),
desc: tRooms('room_1.desc'),
img: 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?auto=format&fit=crop&q=80'
},
{
id: '1-1-daire-a',
title: tRooms('room_2.title'),
desc: tRooms('room_2.desc'),
img: 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?auto=format&fit=crop&q=80'
},
{
id: '1-1-daire-b',
title: tRooms('room_3.title'),
desc: tRooms('room_3.desc'),
img: 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?auto=format&fit=crop&q=80'
}
].map((room, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-50px' }}
transition={{ duration: 0.6, delay: idx * 0.15, ease: EASE }}
className="group bg-surface-container-lowest rounded-[24px] overflow-hidden shadow-[0_32px_64px_-12px_rgba(112,90,76,0.04)] border border-outline-variant/20 transition-transform duration-500 hover:-translate-y-2 flex flex-col cursor-pointer"
>
<div className="aspect-[3/2] overflow-hidden relative">
<Image
src={room.img}
alt={room.title}
fill
className="object-cover transition-transform duration-700 group-hover:scale-110"
/>
</div>
<div className="p-6 flex flex-col flex-1 bg-surface-container-lowest relative z-10">
<h3 className="font-serif text-[24px] font-medium text-secondary mb-3 group-hover:text-primary transition-colors">{room.title}</h3>
<p className="font-sans text-on-surface-variant mb-6 text-[16px] leading-relaxed flex-1">
{room.desc}
</p>
<Link
href={`/dairelerimiz/${room.id}`}
className="inline-flex items-center text-primary font-sans font-bold text-[16px] border-b-2 border-transparent hover:border-primary transition-colors pb-1 w-fit cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-sm"
>
Detayları İncele <span className="ml-2 group-hover:translate-x-1 transition-transform"></span>
</Link>
</div>
</motion.div>
))}
</div>
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.4 }}
className="text-center mt-16"
>
<Link
href="/dairelerimiz"
className="inline-flex items-center justify-center px-10 py-4 border-2 border-primary text-primary hover:bg-primary hover:text-white rounded-full font-sans font-bold text-[16px] uppercase tracking-wider transition-all duration-300 cursor-pointer focus:outline-none focus-visible:ring-4 focus-visible:ring-primary/50 focus-visible:ring-offset-2"
>
Tüm Daireleri Gör
</Link>
</motion.div>
</section>
</div>
)
}