first commit

This commit is contained in:
2026-07-30 12:15:17 +03:00
commit 1f29eead28
48 changed files with 10666 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
import { useTranslations } from 'next-intl'
import Image from 'next/image'
import Link from 'next/link'
import { Instagram, Mail, Phone } from 'lucide-react'
interface FooterProps {
locale: string
}
export default function Footer({ locale }: FooterProps) {
const t = useTranslations('footer')
return (
<footer className="bg-vesta-dark border-t border-white/8 py-12">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-8">
{/* Logo */}
<Link href={`/${locale}`}>
<Image
src="https://cdn.prod.website-files.com/6693a42300f08d15d3514511/6694e59f468a02af6267826d_H%20FULL%20LOGO%20-%20WHITE.png"
alt="Vesta Muğla"
width={140}
height={35}
className="h-7 w-auto object-contain opacity-80"
/>
</Link>
{/* Linkler */}
<div className="flex items-center gap-6">
<a
href="tel:+904443145"
className="text-white/40 hover:text-white/80 transition-colors"
>
<Phone className="w-4 h-4" />
</a>
<a
href="mailto:vestamugla@gmail.com"
className="text-white/40 hover:text-white/80 transition-colors"
>
<Mail className="w-4 h-4" />
</a>
<a
href="https://www.instagram.com/vestamugla/"
target="_blank"
rel="noopener noreferrer"
className="text-white/40 hover:text-white/80 transition-colors"
>
<Instagram className="w-4 h-4" />
</a>
</div>
{/* Copyright */}
<p className="text-white/30 text-xs text-center md:text-right">
{t('rights')}
</p>
</div>
</div>
</footer>
)
}
+130
View File
@@ -0,0 +1,130 @@
'use client'
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import Link from 'next/link'
import Image from 'next/image'
import { Menu, X, Phone } from 'lucide-react'
import { cn } from '@/lib/utils'
interface NavigationProps {
locale: string
}
export default function Navigation({ locale }: NavigationProps) {
const t = useTranslations('nav')
const [menuOpen, setMenuOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 80)
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
const otherLocale = locale === 'tr' ? 'en' : 'tr'
const links = [
{ key: 'about', href: '#proje' },
{ key: 'location', href: '#lokasyon' },
{ key: 'units', href: '#daireler' },
{ key: 'gallery', href: '#galeri' },
{ key: 'contact', href: '#iletisim' },
]
return (
<header
className={cn(
'fixed top-0 left-0 right-0 z-50 transition-all duration-500',
scrolled
? 'bg-vesta-dark/95 backdrop-blur-md py-3 shadow-lg'
: 'bg-transparent py-5'
)}
>
<div className="container mx-auto px-6 flex items-center justify-between">
{/* Logo */}
<Link href={`/${locale}`} className="flex items-center">
<Image
src="https://cdn.prod.website-files.com/6693a42300f08d15d3514511/6694e59f468a02af6267826d_H%20FULL%20LOGO%20-%20WHITE.png"
alt="Vesta Muğla"
width={160}
height={40}
className="h-8 w-auto object-contain"
/>
</Link>
{/* Desktop nav */}
<nav className="hidden md:flex items-center gap-8">
{links.map((link) => (
<a
key={link.key}
href={link.href}
className="text-white/80 hover:text-white text-sm tracking-wide transition-colors duration-200"
>
{t(link.key as keyof ReturnType<typeof t>)}
</a>
))}
</nav>
{/* Right side */}
<div className="hidden md:flex items-center gap-4">
{/* Dil seçici */}
<Link
href={`/${otherLocale}`}
className="text-white/60 hover:text-white text-xs tracking-widest uppercase transition-colors"
>
{otherLocale}
</Link>
{/* Telefon */}
<a
href="tel:+904443145"
className="flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white px-4 py-2 rounded-full text-sm tracking-wide transition-all duration-200"
>
<Phone className="w-3.5 h-3.5" />
{t('phone')}
</a>
</div>
{/* Mobile hamburger */}
<button
className="md:hidden text-white p-2"
onClick={() => setMenuOpen(!menuOpen)}
>
{menuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
{/* Mobile menu */}
{menuOpen && (
<div className="md:hidden bg-vesta-dark/98 backdrop-blur-md border-t border-white/10">
<div className="container mx-auto px-6 py-6 flex flex-col gap-4">
{links.map((link) => (
<a
key={link.key}
href={link.href}
onClick={() => setMenuOpen(false)}
className="text-white/80 hover:text-white text-base py-2 border-b border-white/10 transition-colors"
>
{t(link.key as keyof ReturnType<typeof t>)}
</a>
))}
<div className="flex items-center justify-between pt-2">
<a href="tel:+904443145" className="text-white flex items-center gap-2">
<Phone className="w-4 h-4" />
{t('phone')}
</a>
<Link
href={`/${otherLocale}`}
className="text-white/50 uppercase text-sm"
onClick={() => setMenuOpen(false)}
>
{otherLocale}
</Link>
</div>
</div>
</div>
)}
</header>
)
}
+96
View File
@@ -0,0 +1,96 @@
'use client'
import { useRef } from 'react'
import { useTranslations } from 'next-intl'
import { motion, useInView } from 'framer-motion'
import Image from 'next/image'
export default function AboutSection() {
const t = useTranslations('about')
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-100px' })
const stats = [
{ label: t('stat1Label'), value: t('stat1Value') },
{ label: t('stat2Label'), value: t('stat2Value') },
{ label: t('stat3Label'), value: t('stat3Value') },
]
return (
<section id="proje" ref={ref} className="bg-vesta-dark py-24 md:py-32">
<div className="container mx-auto px-6">
<div className="grid md:grid-cols-2 gap-16 items-center">
{/* Sol: Görsel */}
<motion.div
initial={{ opacity: 0, x: -40 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.9, ease: 'easeOut' }}
className="relative aspect-[4/5] rounded-2xl overflow-hidden"
>
<Image
src="https://images.unsplash.com/photo-1448375240586-882707db888b?w=800&q=80"
alt="Vesta Muğla - Orman"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-vesta-dark/60 to-transparent" />
{/* Label */}
<div className="absolute bottom-6 left-6">
<span className="text-white/60 text-xs tracking-[0.3em] uppercase">
Emtisi İnşaat
</span>
</div>
</motion.div>
{/* Sağ: İçerik */}
<div>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-vesta-earth text-xs tracking-[0.3em] uppercase mb-4"
>
{t('label')}
</motion.p>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, delay: 0.2 }}
className="text-white text-3xl md:text-4xl font-serif font-light leading-snug mb-6"
>
{t('title')}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7, delay: 0.3 }}
className="text-white/50 text-base leading-relaxed mb-12"
>
{t('desc')}
</motion.p>
{/* İstatistikler */}
<div className="grid grid-cols-3 gap-6">
{stats.map((stat, i) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.4 + i * 0.1 }}
className="border-t border-white/10 pt-4"
>
<p className="text-white text-2xl md:text-3xl font-serif font-light">
{stat.value}
</p>
<p className="text-white/40 text-xs mt-1 tracking-wide">{stat.label}</p>
</motion.div>
))}
</div>
</div>
</div>
</div>
</section>
)
}
+104
View File
@@ -0,0 +1,104 @@
'use client'
import { useRef } from 'react'
import { useTranslations } from 'next-intl'
import { motion, useInView } from 'framer-motion'
import Image from 'next/image'
import { TreePine, Users, Dumbbell, Wind } from 'lucide-react'
const amenities = [
{
key: 'livingAreas' as const,
descKey: 'livingDesc' as const,
icon: Users,
image: 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800&q=80',
},
{
key: 'playground' as const,
descKey: 'playgroundDesc' as const,
icon: Wind,
image: 'https://images.unsplash.com/photo-1575783970733-1aaedde1db74?w=800&q=80',
},
{
key: 'sports' as const,
descKey: 'sportsDesc' as const,
icon: Dumbbell,
image: 'https://images.unsplash.com/photo-1534438327276-14e5300c3a48?w=800&q=80',
},
{
key: 'forest' as const,
descKey: 'forestDesc' as const,
icon: TreePine,
image: 'https://images.unsplash.com/photo-1448375240586-882707db888b?w=800&q=80',
},
]
export default function AmenitiesSection() {
const t = useTranslations('amenities')
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-80px' })
return (
<section className="bg-vesta-cream py-24 md:py-32">
<div ref={ref} className="container mx-auto px-6">
{/* Başlık */}
<div className="text-center mb-16">
<motion.p
initial={{ opacity: 0, y: 15 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
className="text-vesta-earth text-xs tracking-[0.3em] uppercase mb-3"
>
{t('label')}
</motion.p>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-vesta-dark text-3xl md:text-5xl font-serif font-light"
>
{t('title')}
</motion.h2>
</div>
{/* Grid */}
<div className="grid md:grid-cols-2 gap-6">
{amenities.map((item, i) => {
const Icon = item.icon
return (
<motion.div
key={item.key}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.15 * i }}
className="relative rounded-2xl overflow-hidden group cursor-default"
>
{/* Arka plan görseli */}
<div className="relative aspect-[16/9]">
<Image
src={item.image}
alt={t(item.key)}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
</div>
{/* İçerik */}
<div className="absolute bottom-0 left-0 right-0 p-6">
<div className="flex items-center gap-3 mb-2">
<Icon className="w-4 h-4 text-white/70" />
<h3 className="text-white font-medium text-lg">{t(item.key)}</h3>
</div>
<p className="text-white/60 text-sm leading-relaxed max-w-md">
{t(item.descKey)}
</p>
</div>
</motion.div>
)
})}
</div>
</div>
</section>
)
}
+171
View File
@@ -0,0 +1,171 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { useTranslations } from 'next-intl'
const TOTAL_FRAMES = 298
const FRAME_PATH = (n: number) =>
`/frames/ezgif-frame-${String(n).padStart(3, '0')}.jpg`
export default function CloudRevealSection() {
const t = useTranslations('hero')
const sectionRef = useRef<HTMLDivElement>(null)
const canvasRef = useRef<HTMLCanvasElement>(null)
const textRef = useRef<HTMLDivElement>(null)
const framesRef = useRef<HTMLImageElement[]>([])
const currentFrameRef = useRef(0)
const rafRef = useRef<number | null>(null)
const [loaded, setLoaded] = useState(false)
const [loadProgress, setLoadProgress] = useState(0)
// Frame'leri sıralı preload et
useEffect(() => {
const images: HTMLImageElement[] = new Array(TOTAL_FRAMES)
let loadedCount = 0
for (let i = 0; i < TOTAL_FRAMES; i++) {
const img = new Image()
img.src = FRAME_PATH(i + 1)
img.onload = () => {
loadedCount++
setLoadProgress(Math.round((loadedCount / TOTAL_FRAMES) * 100))
if (loadedCount === TOTAL_FRAMES) {
framesRef.current = images
setLoaded(true)
// İlk frame'i hemen çiz
drawFrame(0, images)
}
}
images[i] = img
}
}, [])
function drawFrame(index: number, images?: HTMLImageElement[]) {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
const imgs = images || framesRef.current
const img = imgs[index]
if (!img || !img.complete || img.naturalWidth === 0) return
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
}
// Scroll handler
useEffect(() => {
if (!loaded) return
const section = sectionRef.current
const text = textRef.current
if (!section || !text) return
const handleScroll = () => {
const rect = section.getBoundingClientRect()
const sectionHeight = section.offsetHeight
const windowH = window.innerHeight
const scrolled = -rect.top
const total = sectionHeight - windowH
const progress = Math.min(Math.max(scrolled / total, 0), 1)
// Hangi frame'i göstereceğimizi hesapla
const frameIndex = Math.min(
Math.floor(progress * (TOTAL_FRAMES - 1)),
TOTAL_FRAMES - 1
)
// Sadece frame değiştiyse çiz (RAF ile)
if (frameIndex !== currentFrameRef.current) {
currentFrameRef.current = frameIndex
if (rafRef.current) cancelAnimationFrame(rafRef.current)
rafRef.current = requestAnimationFrame(() => drawFrame(frameIndex))
}
// Tagline: ilk %15'te görünür, sonra solar
if (progress < 0.15) {
const opacity = 1 - progress / 0.15
text.style.opacity = String(opacity)
text.style.transform = `translateY(${progress * 40}px)`
} else {
text.style.opacity = '0'
}
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll)
if (rafRef.current) cancelAnimationFrame(rafRef.current)
}
}, [loaded])
// Canvas boyutunu ekrana sığdır
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const resize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
// Resize sonrası mevcut frame'i yeniden çiz
drawFrame(currentFrameRef.current)
}
resize()
window.addEventListener('resize', resize)
return () => window.removeEventListener('resize', resize)
}, [loaded])
return (
<div ref={sectionRef} style={{ height: '500vh' }}>
<div className="sticky top-0 h-screen w-full overflow-hidden bg-black">
{/* Canvas — frame'ler buraya çizilir */}
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
style={{ objectFit: 'cover' }}
/>
{/* Loading ekranı */}
{!loaded && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black z-20">
<div className="w-48 h-px bg-white/10 mb-3">
<div
className="h-full bg-white/60 transition-all duration-150"
style={{ width: `${loadProgress}%` }}
/>
</div>
<p className="text-white/30 text-xs tracking-widest uppercase">
{loadProgress}%
</p>
</div>
)}
{/* Tagline */}
<div
ref={textRef}
className="absolute bottom-16 left-0 right-0 z-10 text-center px-6"
style={{ willChange: 'opacity, transform', transition: 'none' }}
>
<p className="text-white/60 text-xs tracking-[0.4em] uppercase mb-3">
Vesta Muğla
</p>
<h1 className="text-white text-4xl md:text-6xl font-serif font-light leading-tight drop-shadow-2xl">
{t('tagline')}
</h1>
<p className="text-white/50 mt-4 text-base md:text-lg font-light">
{t('subtitle')}
</p>
</div>
{/* Scroll indicator */}
{loaded && (
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-10 flex flex-col items-center gap-2">
<span className="text-white/30 text-xs tracking-widest uppercase">
{t('scroll')}
</span>
<div className="w-px h-10 bg-white/20 animate-pulse" />
</div>
)}
</div>
</div>
)
}
+189
View File
@@ -0,0 +1,189 @@
'use client'
import { useRef, useState } from 'react'
import { useTranslations } from 'next-intl'
import { motion, useInView } from 'framer-motion'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { Phone, Mail, Instagram, Send } from 'lucide-react'
import { ContactSchema, type ContactInput } from '@/lib/validations'
export default function ContactSection() {
const t = useTranslations('contact')
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-80px' })
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
const { register, handleSubmit, reset, formState: { errors } } = useForm<ContactInput>({
resolver: zodResolver(ContactSchema),
})
const onSubmit = async (data: ContactInput) => {
setStatus('loading')
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
if (!res.ok) throw new Error()
setStatus('success')
reset()
} catch {
setStatus('error')
}
}
return (
<section id="iletisim" className="bg-vesta-cream py-24 md:py-32">
<div ref={ref} className="container mx-auto px-6">
<div className="grid md:grid-cols-2 gap-16 items-start">
{/* Sol: Bilgi */}
<div>
<motion.p
initial={{ opacity: 0, y: 15 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
className="text-vesta-earth text-xs tracking-[0.3em] uppercase mb-4"
>
{t('label')}
</motion.p>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-vesta-dark text-3xl md:text-4xl font-serif font-light leading-snug mb-4"
>
{t('title')}
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-vesta-dark/50 text-base mb-10"
>
{t('subtitle')}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-4"
>
<a
href={`tel:+904443145`}
className="flex items-center gap-4 text-vesta-dark hover:text-vesta-earth transition-colors group"
>
<div className="w-10 h-10 rounded-full bg-vesta-dark/10 flex items-center justify-center group-hover:bg-vesta-earth/20 transition-colors">
<Phone className="w-4 h-4" />
</div>
<span className="text-lg">{t('phone')}</span>
</a>
<a
href={`mailto:${t('email')}`}
className="flex items-center gap-4 text-vesta-dark hover:text-vesta-earth transition-colors group"
>
<div className="w-10 h-10 rounded-full bg-vesta-dark/10 flex items-center justify-center group-hover:bg-vesta-earth/20 transition-colors">
<Mail className="w-4 h-4" />
</div>
<span className="text-lg">{t('email')}</span>
</a>
<a
href="https://www.instagram.com/vestamugla/"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-4 text-vesta-dark hover:text-vesta-earth transition-colors group"
>
<div className="w-10 h-10 rounded-full bg-vesta-dark/10 flex items-center justify-center group-hover:bg-vesta-earth/20 transition-colors">
<Instagram className="w-4 h-4" />
</div>
<span className="text-lg">{t('instagram')}</span>
</a>
{/* Sanal Tur */}
<a
href="https://emtisiinsaat.com/vr.html"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 mt-4 border border-vesta-dark/20 text-vesta-dark px-5 py-2.5 rounded-full text-sm hover:bg-vesta-dark hover:text-white transition-colors"
>
360° Sanal Tur
</a>
</motion.div>
</div>
{/* Sağ: Form */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.7, delay: 0.2 }}
className="bg-white rounded-2xl p-8 shadow-sm"
>
{status === 'success' ? (
<div className="text-center py-8">
<div className="w-12 h-12 rounded-full bg-vesta-forest/20 flex items-center justify-center mx-auto mb-4">
<Send className="w-5 h-5 text-vesta-forest" />
</div>
<p className="text-vesta-dark font-medium">{t('form.success')}</p>
</div>
) : (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<div>
<input
{...register('fullName')}
placeholder={t('form.name')}
className="w-full border border-gray-200 rounded-xl px-4 py-3 text-sm text-vesta-dark placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-vesta-earth/30 focus:border-vesta-earth"
/>
{errors.fullName && (
<p className="text-red-500 text-xs mt-1">{errors.fullName.message}</p>
)}
</div>
<div>
<input
{...register('email')}
type="email"
placeholder={t('form.email')}
className="w-full border border-gray-200 rounded-xl px-4 py-3 text-sm text-vesta-dark placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-vesta-earth/30 focus:border-vesta-earth"
/>
{errors.email && (
<p className="text-red-500 text-xs mt-1">{errors.email.message}</p>
)}
</div>
<div>
<input
{...register('phone')}
type="tel"
placeholder={t('form.phone')}
className="w-full border border-gray-200 rounded-xl px-4 py-3 text-sm text-vesta-dark placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-vesta-earth/30 focus:border-vesta-earth"
/>
</div>
<div>
<textarea
{...register('message')}
placeholder={t('form.message')}
rows={4}
className="w-full border border-gray-200 rounded-xl px-4 py-3 text-sm text-vesta-dark placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-vesta-earth/30 focus:border-vesta-earth resize-none"
/>
{errors.message && (
<p className="text-red-500 text-xs mt-1">{errors.message.message}</p>
)}
</div>
{status === 'error' && (
<p className="text-red-500 text-xs">{t('form.error')}</p>
)}
<button
type="submit"
disabled={status === 'loading'}
className="w-full bg-vesta-dark text-white py-3.5 rounded-xl text-sm tracking-wide hover:bg-vesta-forest transition-colors disabled:opacity-50"
>
{status === 'loading' ? t('form.sending') : t('form.send')}
</button>
</form>
)}
</motion.div>
</div>
</div>
</section>
)
}
+53
View File
@@ -0,0 +1,53 @@
'use client'
import { useRef } from 'react'
import { useTranslations } from 'next-intl'
import { motion, useInView } from 'framer-motion'
import Image from 'next/image'
import { MOCK_GALLERY } from '@/lib/mock'
export default function GallerySection() {
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-80px' })
return (
<section id="galeri" className="bg-vesta-dark py-24 md:py-32">
<div ref={ref} className="container mx-auto px-6">
<motion.p
initial={{ opacity: 0, y: 15 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
className="text-vesta-earth text-xs tracking-[0.3em] uppercase mb-12 text-center"
>
Galeri
</motion.p>
{/* Masonry-like grid */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{MOCK_GALLERY.map((item, i) => (
<motion.div
key={item.id}
initial={{ opacity: 0, scale: 0.97 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ duration: 0.5, delay: 0.07 * i }}
className={`relative overflow-hidden rounded-xl group ${
i === 0 ? 'col-span-2 md:col-span-2 row-span-2 aspect-[4/3]' : 'aspect-square'
}`}
>
<Image
src={item.imageUrl}
alt={item.titleTr}
fill
className="object-cover transition-transform duration-700 group-hover:scale-108"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors duration-300" />
<div className="absolute bottom-3 left-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<p className="text-white text-sm font-medium">{item.titleTr}</p>
</div>
</motion.div>
))}
</div>
</div>
</section>
)
}
+91
View File
@@ -0,0 +1,91 @@
'use client'
import { useRef } from 'react'
import { useTranslations } from 'next-intl'
import { motion, useInView } from 'framer-motion'
import { Waves, GraduationCap, Building2, Heart, Plane, Landmark } from 'lucide-react'
const locationItems = [
{ key: 'beach' as const, itemsKey: 'beachItems' as const, icon: Waves },
{ key: 'education' as const, itemsKey: 'educationItems' as const, icon: GraduationCap },
{ key: 'city' as const, itemsKey: 'cityItems' as const, icon: Building2 },
{ key: 'health' as const, itemsKey: 'healthItems' as const, icon: Heart },
{ key: 'airport' as const, itemsKey: 'airportItems' as const, icon: Plane },
{ key: 'tourism' as const, itemsKey: 'tourismItems' as const, icon: Landmark },
]
export default function LocationSection() {
const t = useTranslations('location')
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-80px' })
return (
<section id="lokasyon" className="bg-vesta-dark py-24 md:py-32">
<div ref={ref} className="container mx-auto px-6">
<div className="grid md:grid-cols-2 gap-16 items-start">
{/* Sol: Başlık + Grid */}
<div>
<motion.p
initial={{ opacity: 0, y: 15 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
className="text-vesta-earth text-xs tracking-[0.3em] uppercase mb-4"
>
{t('label')}
</motion.p>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-white text-3xl md:text-4xl font-serif font-light leading-snug mb-12"
>
{t('title')}
</motion.h2>
<div className="grid grid-cols-1 gap-5">
{locationItems.map((item, i) => {
const Icon = item.icon
return (
<motion.div
key={item.key}
initial={{ opacity: 0, x: -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.5, delay: 0.15 + i * 0.08 }}
className="flex items-start gap-4 border-b border-white/8 pb-5"
>
<div className="w-9 h-9 rounded-full bg-vesta-forest/30 flex items-center justify-center shrink-0 mt-0.5">
<Icon className="w-4 h-4 text-vesta-earth" />
</div>
<div>
<p className="text-white/80 text-sm font-medium mb-1">{t(item.key)}</p>
<p className="text-white/40 text-xs leading-relaxed">{t(item.itemsKey)}</p>
</div>
</motion.div>
)
})}
</div>
</div>
{/* Sağ: Harita embed */}
<motion.div
initial={{ opacity: 0, x: 40 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ duration: 0.8, delay: 0.2 }}
className="rounded-2xl overflow-hidden aspect-square md:aspect-auto md:h-[600px] sticky top-24"
>
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d50536.60905254124!2d28.3200!3d37.2153!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x14be5ce7c5a5ccdb%3A0x1e3eb12f3c0f3c0a!2sMu%C4%9Fla%2C%20T%C3%BCrkiye!5e1!3m2!1str!2str!4v1700000000000"
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
className="grayscale"
/>
</motion.div>
</div>
</div>
</section>
)
}
+120
View File
@@ -0,0 +1,120 @@
'use client'
import { useRef } from 'react'
import { useTranslations } from 'next-intl'
import { motion, useInView } from 'framer-motion'
import Image from 'next/image'
import { BedDouble, Bath, Maximize2 } from 'lucide-react'
import { MOCK_UNITS } from '@/lib/mock'
export default function UnitsSection() {
const t = useTranslations('units')
const ref = useRef<HTMLDivElement>(null)
const isInView = useInView(ref, { once: true, margin: '-80px' })
return (
<section id="daireler" className="bg-vesta-cream py-24 md:py-32">
<div ref={ref} className="container mx-auto px-6">
{/* Başlık */}
<div className="text-center mb-16">
<motion.p
initial={{ opacity: 0, y: 15 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
className="text-vesta-earth text-xs tracking-[0.3em] uppercase mb-3"
>
{t('label')}
</motion.p>
<motion.h2
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-vesta-dark text-3xl md:text-5xl font-serif font-light"
>
{t('title')}
</motion.h2>
</div>
{/* Daire listesi */}
<div className="grid md:grid-cols-2 gap-8">
{MOCK_UNITS.map((unit, i) => (
<motion.div
key={unit.id}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.1 * i }}
className="group bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-shadow duration-400"
>
{/* Görsel */}
<div className="relative aspect-video overflow-hidden">
<Image
src={unit.imageUrl || 'https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?w=800'}
alt={unit.typeTr}
fill
className="object-cover transition-transform duration-700 group-hover:scale-105"
/>
{/* Müsaitlik badge */}
<div className={`absolute top-4 right-4 px-3 py-1 rounded-full text-xs font-medium ${
unit.available
? 'bg-vesta-forest text-white'
: 'bg-gray-400 text-white'
}`}>
{unit.available ? t('available') : t('notAvailable')}
</div>
</div>
{/* İçerik */}
<div className="p-6">
<div className="flex items-start justify-between mb-3">
<h3 className="text-vesta-dark text-xl font-serif font-medium">
{unit.typeTr}
</h3>
<span className="text-vesta-earth text-2xl font-serif font-light">
{unit.size} <span className="text-sm">{t('sqm')}</span>
</span>
</div>
<p className="text-vesta-dark/60 text-sm leading-relaxed mb-5 line-clamp-2">
{unit.descTr}
</p>
{/* Özellikler */}
<div className="flex items-center gap-5 pt-4 border-t border-gray-100">
<div className="flex items-center gap-1.5 text-vesta-dark/50 text-sm">
<BedDouble className="w-4 h-4" />
<span>{unit.rooms} {t('rooms')}</span>
</div>
<div className="flex items-center gap-1.5 text-vesta-dark/50 text-sm">
<Bath className="w-4 h-4" />
<span>{unit.bathrooms} {t('bath')}</span>
</div>
<div className="flex items-center gap-1.5 text-vesta-dark/50 text-sm">
<Maximize2 className="w-4 h-4" />
<span>{unit.size} {t('sqm')}</span>
</div>
</div>
</div>
</motion.div>
))}
</div>
{/* Katalog CTA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, delay: 0.5 }}
className="text-center mt-12"
>
<a
href="https://indd.adobe.com/view/6b17e969-ae44-481c-861f-684806e8c3b2"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 bg-vesta-dark text-white px-8 py-4 rounded-full text-sm tracking-wide hover:bg-vesta-forest transition-colors duration-300"
>
Proje Kataloğunu İncele
</a>
</motion.div>
</div>
</section>
)
}