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>
)
}
+131 -104
View File
@@ -6,115 +6,142 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--color-foreground: var(--on-background);
--font-sans: var(--font-nunito-sans);
--font-serif: var(--font-literata);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--font-heading: var(--font-literata);
--color-surface: var(--surface);
--color-surface-dim: var(--surface-dim);
--color-surface-bright: var(--surface-bright);
--color-surface-container-lowest: var(--surface-container-lowest);
--color-surface-container-low: var(--surface-container-low);
--color-surface-container: var(--surface-container);
--color-surface-container-high: var(--surface-container-high);
--color-surface-container-highest: var(--surface-container-highest);
--color-on-surface: var(--on-surface);
--color-on-surface-variant: var(--on-surface-variant);
--color-inverse-surface: var(--inverse-surface);
--color-inverse-on-surface: var(--inverse-on-surface);
--color-outline: var(--outline);
--color-outline-variant: var(--outline-variant);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
--color-on-primary: var(--on-primary);
--color-primary-container: var(--primary-container);
--color-on-primary-container: var(--on-primary-container);
--color-inverse-primary: var(--inverse-primary);
--color-secondary: var(--secondary);
--color-on-secondary: var(--on-secondary);
--color-secondary-container: var(--secondary-container);
--color-on-secondary-container: var(--on-secondary-container);
--color-tertiary: var(--tertiary);
--color-on-tertiary: var(--on-tertiary);
--color-tertiary-container: var(--tertiary-container);
--color-on-tertiary-container: var(--on-tertiary-container);
--color-error: var(--error);
--color-on-error: var(--on-error);
--color-error-container: var(--error-container);
--color-on-error-container: var(--on-error-container);
--color-accent-pink: var(--accent-pink);
/* shadcn mappings */
--color-border: var(--outline-variant);
--color-input: var(--outline-variant);
--color-ring: var(--primary);
--color-muted: var(--surface-container-highest);
--color-muted-foreground: var(--on-surface-variant);
--color-accent: var(--tertiary);
--color-accent-foreground: var(--on-tertiary);
--color-card: var(--surface);
--color-card-foreground: var(--on-surface);
--color-popover: var(--surface);
--color-popover-foreground: var(--on-surface);
--color-destructive: var(--error);
--color-destructive-foreground: var(--on-error);
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--radius-lg: 1rem;
--radius-xl: 1.5rem;
--radius-2xl: 2rem;
--radius-full: 9999px;
--spacing-unit: 8px;
--spacing-container-max: 1200px;
--spacing-gutter: 24px;
--spacing-margin-mobile: 20px;
--spacing-section-gap: 80px;
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
/* Aegean Coastal Sanctuary Palette */
--surface: #f6fafb;
--surface-dim: #d7dbdc;
--surface-bright: #f6fafb;
--surface-container-lowest: #ffffff;
--surface-container-low: #f1f4f5;
--surface-container: #ebeef0;
--surface-container-high: #e5e9ea;
--surface-container-highest: #dfe3e4;
--on-surface: #181c1d;
--on-surface-variant: #3e494b;
--inverse-surface: #2d3132;
--inverse-on-surface: #eef1f2;
--outline: #6e797c;
--outline-variant: #bdc8cb;
--surface-tint: #006877;
--primary: #006574;
--on-primary: #ffffff;
--primary-container: #088092;
--on-primary-container: #f8fdff;
--inverse-primary: #78d4e7;
--secondary: #705a4c;
--on-secondary: #ffffff;
--secondary-container: #f8dac8;
--on-secondary-container: #755e50;
--tertiary: #8a4c17;
--on-tertiary: #ffffff;
--tertiary-container: #a7642e;
--on-tertiary-container: #fffbff;
--error: #ba1a1a;
--on-error: #ffffff;
--error-container: #ffdad6;
--on-error-container: #93000a;
--background: #f6fafb;
--on-background: #181c1d;
--accent-pink: #d46a8c;
/* Fluid Typography from Design System */
--text-hero: clamp(3rem, 8vw, 8rem);
--text-h1: 48px;
--text-h2: 32px;
--text-h3: 24px;
--text-body-lg: 18px;
--text-body-md: 16px;
--text-label-lg: 14px;
}
/* For simplicity, map dark mode to slightly adjusted values if needed, or leave as is if dark mode isn't explicitly defined in design.md */
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--background: #181c1d;
--on-background: #f6fafb;
--surface: #181c1d;
--on-surface: #e5e9ea;
--primary: #78d4e7;
--on-primary: #00363f;
--secondary: #dec1af;
--on-secondary: #3e2d22;
--tertiary: #ffb782;
--on-tertiary: #4c2700;
--outline-variant: #3e494b;
}
@layer base {
@@ -124,7 +151,7 @@
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
h1 { font-size: var(--text-h1); line-height: 1.2; font-weight: 600; }
h2 { font-size: var(--text-h2); line-height: 1.3; font-weight: 600; }
h3 { font-size: var(--text-h3); line-height: 1.4; font-weight: 500; }
}