feat: complete initial project setup with i18n and standalone config

This commit is contained in:
2026-04-15 22:36:48 +03:00
parent 66f0657fc2
commit de89099b4f
154 changed files with 3350 additions and 119 deletions

123
components/Footer.tsx Normal file
View File

@@ -0,0 +1,123 @@
'use client';
import { Link } from '@/i18n/routing';
export default function Footer() {
return (
<footer className="bg-aegean-dark text-bone py-20 px-6 font-sans">
<div className="max-w-7xl mx-auto">
{/* Top Centered Section */}
<div className="text-center mb-20">
<h2 className="text-5xl md:text-7xl font-serif tracking-[0.2em] text-sand mb-4">SALMAKIS</h2>
<p className="text-[10px] tracking-[0.4em] uppercase opacity-60 mb-10">
A Heritage of Excellence Since 1980.
</p>
<div className="space-y-2 text-sm opacity-80 font-medium">
<p>+90 252 316 65 06</p>
<p>08:00 - 20:00</p>
<p>salmakis@salmakis.com.tr</p>
</div>
</div>
{/* Three Columns Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-16 text-center mb-24">
{/* Column 1: Resort */}
<div className="space-y-6">
<h3 className="text-xl font-serif text-sand tracking-wide">Salmakis Resort</h3>
<div className="text-[10px] leading-relaxed tracking-widest uppercase opacity-60 space-y-1">
<p>BARDAKÇI KOYU</p>
<p>BODRUM/MUĞLA/TURKEY</p>
</div>
<div className="text-xs space-y-1 opacity-80">
<p>+90 252 316 65 06</p>
<p>+90 252 316 65 07</p>
<p>+90 252 316 65 11</p>
</div>
<p className="text-[10px] opacity-40">salmakis@salmakis.com.tr</p>
</div>
{/* Column 2: Villas */}
<div className="space-y-6">
<h3 className="text-xl font-serif text-sand tracking-wide">Salmakis Villas</h3>
<div className="text-[10px] leading-relaxed tracking-widest uppercase opacity-60 space-y-1">
<p>BADEMLİK MEVKİİ</p>
<p>KÜME EVLERİ NO 24</p>
<p>BODRUM/MUĞLA/TURKEY</p>
</div>
<div className="text-xs space-y-1 opacity-80">
<p>+90 252 316 27 38</p>
<p>+90 252 316 28 77</p>
<p>+90 532 731 78 04</p>
<p>+90 252 316 27 37</p>
</div>
<p className="text-[10px] opacity-40">info@salmakisvillas.com</p>
</div>
{/* Column 3: Yachting */}
<div className="space-y-6">
<h3 className="text-xl font-serif text-sand tracking-wide">Salmakis Yachting</h3>
<div className="text-[10px] leading-relaxed tracking-widest uppercase opacity-60 space-y-1">
<p>KUMBAHÇE MAH. İÇMELER CAD.</p>
<p>NO 28/1</p>
<p>BODRUM/MUĞLA/TURKEY</p>
</div>
<div className="text-xs space-y-1 opacity-80">
<p>+90 252 316 27 38</p>
<p>+90 252 316 28 77</p>
<p>+90 252 316 27 37</p>
</div>
<p className="text-[10px] opacity-40">info@salmakisyachting.com</p>
</div>
</div>
{/* Socials Section */}
<div className="flex flex-col md:flex-row justify-end items-center gap-6 mb-12 border-t border-white/5 pt-12">
<span className="text-[10px] font-bold tracking-[0.3em] uppercase opacity-60">FOLLOW US</span>
<div className="flex items-center space-x-6">
{/* Instagram */}
<svg className="w-5 h-5 opacity-80 hover:opacity-100 cursor-pointer transition-opacity" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
{/* Facebook */}
<svg className="w-5 h-5 opacity-80 hover:opacity-100 cursor-pointer transition-opacity" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
{/* Play/Video */}
<svg className="w-5 h-5 opacity-80 hover:opacity-100 cursor-pointer transition-opacity" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<polygon points="10 8 16 12 10 16 10 8"></polygon>
</svg>
{/* LinkedIn */}
<svg className="w-5 h-5 opacity-80 hover:opacity-100 cursor-pointer transition-opacity" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</div>
</div>
{/* Bottom Bar */}
<div className="flex flex-col md:flex-row justify-between items-center gap-8 pt-12 border-t border-white/5">
<div className="flex flex-wrap justify-center gap-8 text-[9px] font-bold tracking-[0.2em] uppercase opacity-50">
<Link href="#" className="hover:opacity-100 transition-opacity">LEGAL & PRIVACY</Link>
<Link href="#" className="hover:opacity-100 transition-opacity">COOKIES</Link>
<Link href="#" className="hover:opacity-100 transition-opacity">SITEMAP</Link>
<Link href="#" className="hover:opacity-100 transition-opacity">COOKIES SETTINGS</Link>
</div>
<div className="text-right space-y-2">
<p className="text-[9px] font-bold tracking-[0.2em] uppercase opacity-40">
© SALMAKIS 2026 ALL RIGHTS RESERVED
</p>
<p className="text-[9px] font-bold tracking-[0.2em] uppercase opacity-40">
CREATED BY <span className="text-sand/80">AYRISTECH</span>
</p>
</div>
</div>
</div>
</footer>
);
}

74
components/Hero.tsx Normal file
View File

@@ -0,0 +1,74 @@
'use client';
import { motion } from 'framer-motion';
import { useTranslations } from 'next-intl';
import { Link } from '@/i18n/routing';
import { ChevronRight } from 'lucide-react';
import Image from 'next/image';
export default function Hero() {
const t = useTranslations('Index');
return (
<section className="relative h-screen flex items-center justify-center overflow-hidden">
{/* Background with Overlay */}
<div className="absolute inset-0 z-0">
<Image
src="https://images.unsplash.com/photo-1542718610-a1d656d1884c?q=80&w=2070&auto=format&fit=crop"
alt="Luxury Villa"
fill
priority
className="object-cover scale-105"
/>
<div className="absolute inset-0 bg-black/30 bg-gradient-to-b from-black/40 via-transparent to-bone/10" />
</div>
{/* Content */}
<div className="relative z-10 text-center px-6 max-w-4xl">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<span className="inline-block mb-4 text-bone text-sm font-bold tracking-[0.3em] uppercase opacity-90">
Salmakis Collection
</span>
<h1 className="text-5xl md:text-8xl font-serif text-bone leading-tight mb-6">
{t('title')}
</h1>
<p className="text-lg md:text-xl text-bone/90 font-sans max-w-2xl mx-auto mb-10 leading-relaxed">
{t('description')}
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/#villas"
className="group relative px-8 py-4 bg-salmakis-blue text-white rounded-full font-bold overflow-hidden transition-all duration-300 hover:shadow-xl hover:shadow-salmakis-blue/20"
>
<span className="relative z-10 flex items-center space-x-2">
<span>{t('explore')}</span>
<ChevronRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</span>
</Link>
<Link
href="/contact"
className="px-8 py-4 bg-white/10 backdrop-blur-md border border-white/20 text-white rounded-full font-bold hover:bg-white/20 transition-all duration-300"
>
İletişime Geç
</Link>
</div>
</motion.div>
</div>
{/* Scroll indicator */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1, duration: 1 }}
className="absolute bottom-10 left-1/2 -translate-x-1/2"
>
<div className="w-[1px] h-20 bg-gradient-to-b from-white to-transparent" />
</motion.div>
</section>
);
}

109
components/Navbar.tsx Normal file
View File

@@ -0,0 +1,109 @@
'use client';
import { useTranslations, useLocale } from 'next-intl';
import { Link, usePathname, useRouter } from '@/i18n/routing';
import { useState, useEffect } from 'react';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { Menu, X, Globe } from 'lucide-react';
function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export default function Navbar() {
const t = useTranslations('Navbar');
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const [isScrolled, setIsScrolled] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const toggleLanguage = () => {
const nextLocale = locale === 'tr' ? 'en' : 'tr';
router.replace(pathname, { locale: nextLocale });
};
const navLinks = [
{ href: '/', label: t('villas') },
{ href: '/about', label: t('about') },
{ href: '/contact', label: t('contact') },
];
return (
<nav
className={cn(
'fixed top-0 left-0 right-0 z-50 transition-all duration-300 px-6 py-4',
isScrolled ? 'glass-nav py-3' : 'bg-transparent'
)}
>
<div className="max-w-7xl mx-auto flex items-center justify-between">
<Link href="/" className="text-2xl font-serif font-bold tracking-tight text-aegean-dark">
SALMAKIS <span className="text-salmakis-blue">VILLAS</span>
</Link>
{/* Desktop Nav */}
<div className="hidden md:flex items-center space-x-8">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href as any}
className="text-sm font-medium hover:text-salmakis-blue transition-colors"
>
{link.label}
</Link>
))}
<button
onClick={toggleLanguage}
className="flex items-center space-x-1 text-xs font-bold uppercase tracking-widest border border-aegean-dark/20 px-3 py-1 rounded-full hover:bg-aegean-dark hover:text-bone transition-all"
>
<Globe className="w-3 h-3" />
<span>{locale === 'tr' ? 'EN' : 'TR'}</span>
</button>
</div>
{/* Mobile Toggle */}
<button
className="md:hidden text-aegean-dark"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
>
{isMobileMenuOpen ? <X /> : <Menu />}
</button>
</div>
{/* Mobile Menu */}
{isMobileMenuOpen && (
<div className="md:hidden absolute top-full left-0 right-0 bg-bone border-b border-aegean-dark/10 p-6 flex flex-col space-y-4 animate-in fade-in slide-in-from-top-4">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href as any}
className="text-lg font-serif"
onClick={() => setIsMobileMenuOpen(false)}
>
{link.label}
</Link>
))}
<button
onClick={() => {
toggleLanguage();
setIsMobileMenuOpen(false);
}}
className="flex items-center space-x-2 text-sm font-bold uppercase py-2"
>
<Globe className="w-4 h-4" />
<span>{locale === 'tr' ? 'English' : 'Türkçe'}</span>
</button>
</div>
)}
</nav>
);
}

76
components/VillaCard.tsx Normal file
View File

@@ -0,0 +1,76 @@
'use client';
import { motion } from 'framer-motion';
import { Villa } from '@/data/villas';
import { Link } from '@/i18n/routing';
import { Users, Bed, Droplets, MapPin } from 'lucide-react';
import Image from 'next/image';
interface VillaCardProps {
villa: Villa;
index: number;
}
export default function VillaCard({ villa, index }: VillaCardProps) {
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="villa-card group bg-white rounded-2xl overflow-hidden shadow-sm"
>
<Link href={`/villas/${villa.slug}` as any}>
<div className="relative aspect-[4/5] overflow-hidden">
<Image
src={villa.images[0]}
alt={villa.name}
fill
className="object-cover transition-transform duration-700 group-hover:scale-110"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<div className="absolute top-4 left-4 bg-white/90 backdrop-blur-sm px-3 py-1 rounded-full text-[10px] font-bold tracking-widest uppercase text-aegean-dark">
Portfolio
</div>
<div className="absolute bottom-0 left-0 right-0 p-6 bg-gradient-to-t from-black/60 to-transparent">
<div className="flex items-center text-white/90 text-xs mb-1">
<MapPin className="w-3 h-3 mr-1" />
{villa.location}
</div>
<h3 className="text-2xl font-serif text-white">{villa.name}</h3>
</div>
</div>
</Link>
<div className="p-6">
<div className="flex justify-between items-center mb-6 border-b border-aegean-dark/5 pb-4">
<div className="flex items-center space-x-4">
<div className="flex flex-col items-center">
<Users className="w-4 h-4 text-salmakis-blue mb-1" />
<span className="text-[10px] uppercase font-bold text-aegean-dark/40">{villa.capacity} Kişi</span>
</div>
<div className="flex flex-col items-center">
<Bed className="w-4 h-4 text-salmakis-blue mb-1" />
<span className="text-[10px] uppercase font-bold text-aegean-dark/40">{villa.bedrooms} Oda</span>
</div>
<div className="flex flex-col items-center">
<Droplets className="w-4 h-4 text-salmakis-blue mb-1" />
<span className="text-[10px] uppercase font-bold text-aegean-dark/40">Havuz</span>
</div>
</div>
<div className="text-right">
<span className="text-[10px] uppercase font-bold text-aegean-dark/40 block">Gecelik</span>
<span className="text-lg font-serif font-bold text-salmakis-blue">{villa.priceLow} - {villa.priceHigh}</span>
</div>
</div>
<Link
href={`/villas/${villa.slug}` as any}
className="w-full block py-3 text-center rounded-xl border border-aegean-dark/10 text-sm font-bold uppercase tracking-widest bg-aegean-dark text-white hover:bg-salmakis-blue transition-all duration-300 shadow-lg shadow-aegean-dark/10"
>
Detayları İncele
</Link>
</div>
</motion.div>
);
}

168
components/VillaGallery.tsx Normal file
View File

@@ -0,0 +1,168 @@
'use client';
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, ChevronLeft, ChevronRight, Maximize2 } from 'lucide-react';
import Image from 'next/image';
interface VillaGalleryProps {
images: string[];
name: string;
}
export default function VillaGallery({ images, name }: VillaGalleryProps) {
const [isOpen, setIsOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const openLightbox = (index: number) => {
setCurrentIndex(index);
setIsOpen(true);
document.body.style.overflow = 'hidden';
};
const closeLightbox = () => {
setIsOpen(false);
document.body.style.overflow = 'auto';
};
const nextImage = () => {
setCurrentIndex((prev) => (prev + 1) % images.length);
};
const prevImage = () => {
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
};
return (
<>
<div className="grid grid-cols-1 md:grid-cols-4 grid-rows-2 h-[400px] md:h-[600px] gap-4 rounded-3xl overflow-hidden shadow-2xl">
{/* Main large image */}
<div
className="md:col-span-2 md:row-span-2 relative group overflow-hidden cursor-zoom-in"
onClick={() => openLightbox(0)}
>
<Image
src={images[0]}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
alt={`${name} 1`}
priority
/>
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Maximize2 className="text-white w-8 h-8" />
</div>
</div>
{/* Second image */}
<div
className="md:col-span-1 relative group overflow-hidden cursor-zoom-in"
onClick={() => openLightbox(1)}
>
{images[1] ? (
<Image src={images[1]} fill className="object-cover transition-transform duration-700 group-hover:scale-105" alt={`${name} 2`} />
) : (
<div className="w-full h-full bg-aegean-dark/5" />
)}
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Maximize2 className="text-white w-6 h-6" />
</div>
</div>
{/* Third image */}
<div
className="md:col-span-1 relative group overflow-hidden cursor-zoom-in"
onClick={() => openLightbox(2)}
>
{images[2] ? (
<Image src={images[2]} fill className="object-cover transition-transform duration-700 group-hover:scale-105" alt={`${name} 3`} />
) : (
<div className="w-full h-full bg-aegean-dark/5" />
)}
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<Maximize2 className="text-white w-6 h-6" />
</div>
</div>
{/* Fourth image with overlay if more exists */}
<div
className="md:col-span-2 relative group overflow-hidden cursor-zoom-in"
onClick={() => openLightbox(3)}
>
{images[3] ? (
<Image src={images[3]} fill className="object-cover transition-transform duration-700 group-hover:scale-105" alt={`${name} 4`} />
) : (
<div className="w-full h-full bg-aegean-dark/5" />
)}
{images.length > 4 && (
<div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center text-white backdrop-blur-[2px] group-hover:bg-black/40 transition-all">
<span className="text-3xl font-serif">+{images.length - 4}</span>
<span className="text-[10px] font-bold uppercase tracking-widest mt-2">Daha Fazla Fotoğraf</span>
</div>
)}
<div className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
{! (images.length > 4) && <Maximize2 className="text-white w-8 h-8" />}
</div>
</div>
</div>
{/* Lightbox Overlay */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] bg-black/95 flex items-center justify-center p-4 md:p-8"
>
<button
onClick={closeLightbox}
className="absolute top-8 right-8 text-white/70 hover:text-white z-50 transition-colors"
>
<X className="w-8 h-8" />
</button>
<button
onClick={prevImage}
className="absolute left-4 md:left-8 text-white/70 hover:text-white z-50 p-2 rounded-full hover:bg-white/10 transition-all"
>
<ChevronLeft className="w-10 h-10" />
</button>
<button
onClick={nextImage}
className="absolute right-4 md:right-8 text-white/70 hover:text-white z-50 p-2 rounded-full hover:bg-white/10 transition-all"
>
<ChevronRight className="w-10 h-10" />
</button>
<motion.div
key={currentIndex}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="relative w-full h-full max-w-6xl max-h-[80vh]"
>
{images[currentIndex] ? (
<Image
src={images[currentIndex]}
fill
className="object-contain"
alt={`${name} Full`}
quality={100}
/>
) : (
<div className="flex items-center justify-center h-full text-white/50">Görsel Yüklenemedi</div>
)}
</motion.div>
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 text-white/60 text-sm font-medium tracking-widest uppercase">
{currentIndex + 1} / {images.length} {name}
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}