Compare commits

..

12 Commits

39 changed files with 6467 additions and 196 deletions

58
Dockerfile Normal file
View File

@@ -0,0 +1,58 @@
# 1. Base image
FROM node:20-alpine AS base
# 2. Dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json package-lock.json* ./
RUN npm ci --legacy-peer-deps
# 3. Builder
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Environment variables must be present at build time for Next.js
# Coolify will provide these, but we can set defaults
ENV NEXT_TELEMETRY_DISABLED=1
# Generate Prisma Client
RUN npx prisma generate
RUN npm run build
# 4. Runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# set hostname to localhost
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

25
components.json Normal file
View File

@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

84
docs/.PRD Normal file
View File

@@ -0,0 +1,84 @@
🏗️ Ürün Gereksinim Dokümanı (PRD): Utku Kırkan Web Projesi
1. Ürün Özeti
Utku Kırkan'ın inşaat mühendisliği ve müteahhitlik kimliğini modern, minimalist ve güven veren bir dijital kimlikle temsil eden, reklam odaklı bir Landing Page.
Hedef Kitle: Arsa sahipleri, yatırımcılar, konut arayanlar ve teknik danışmanlık ihtiyacı olanlar.
Temel Amaç: Güven inşa etmek ve hızlı iletişim (Lead Generation) sağlamak.
2. Teknik Stack
Senin uzmanlığın ve mevcut yapına en uygun, yüksek performanslı mimari:
Frontend: Next.js 15 (App Router).
Styling: Tailwind CSS + Framer Motion (Akışkan animasyonlar için).
Database: PostgreSQL (Coolify üzerinde self-hosted).
ORM: Drizzle ORM (Hız ve Tip güvenliği için).
Deployment: Coolify (Dockerized).
3. Sayfa Yapısı ve İçerik (Single Page Architecture)
3.1. Hero Section (İlk İzlenim)
Görsel: Yüksek çözünürlüklü, geniş açılı bir şantiye veya bitmiş proje fotoğrafı (Overlay ile karartılmış).
Metin: "Modern Mühendislik, Güçlü Yapılar: Utku Kırkan".
CTA (Eylem Butonu): "Projelerimizi Keşfedin" ve "Teklif Al".
3.2. Projeler (Dynamic Gallery)
Özellik: Veritabanından çekilen, kart tasarımlı proje listesi.
Detay: Her proje kartında; Proje Adı, Konum (Muğla/Menteşe vb.), Durum (Tamamlandı/Devam Ediyor).
Modern Dokunuş: Resimlerin üzerine gelindiğinde (hover) detayların belirmesi.
3.3. Hizmetler (Core Competencies)
Statik Proje ve Tasarım.
Müteahhitlik ve Anahtar Teslim İnşaat.
Teknik Danışmanlık ve Denetim.
3.4. Hakkında & Güven (Social Proof)
Utku Kırkan'ın kısa özgeçmişi ve "Mühendislik disipliniyle müteahhitlik" vurgusu.
(Varsa) Referans logoları veya çözüm ortakları.
3.5. İletişim (Conversion Zone)
Hızlı Erişim: Doğrudan WhatsApp yönlendirmesi.
Basit Form: Ad-Soyad, Telefon ve Kısa Not (Coolify DB'ye kaydedilecek ve belki sana bir bildirim düşecek).
Lokasyon: Google Haritalar entegrasyonu.
4. Teknik Gereksinimler & Fonksiyonel Özellikler
Özellik Açıklama
Hız (LCP) Next.js Image component ile optimize edilmiş görseller. Google PageSpeed puanı +90 olmalı.
SEO Muğla odaklı anahtar kelimeler (Muğla müteahhit, statik proje mühendisi vb.) meta tag entegrasyonu.
Responsive Mobil öncelikli tasarım. Şantiyede telefondan bakan müşteri için kusursuz deneyim.
Admin Paneli /admin altında Utku'nun (veya senin) yeni proje görseli ve metni ekleyebileceği şifreli, basit bir crud ekranı.
Analytics Reklam verimliliğini ölçmek için basit bir Google Analytics veya Vercel Analytics entegrasyonu.
5. Tasarım İlkeleri
Tipografi: Modern ve maskülen fontlar (Örn: Bebas Neue başlıklar için, Inter gövde metinleri için).
Renkler: * Primary: #111827 (Deep Charcoal)
Accent: #F59E0B (Amber/Construction Gold)
Background: #FFFFFF veya çok açık gri.
Animasyon: Sayfa aşağı kaydırıldığında (scroll) elementlerin yumuşakça belirmesi (Reveal effect).
6. Başarı Kriterleri (KPI)
Sitenin 1.5 saniyenin altında yüklenmesi.
Muğla yerel aramalarında ilk sayfada görünürlük.
Haftalık gelen WhatsApp/Form "lead" sayısı.

51
infographic.html Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>contribution.usercontent.com - Coming Soon</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 60px 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
max-width: 600px;
}
h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 20px;
word-break: break-word;
}
p {
color: #666;
font-size: 1.2em;
line-height: 1.6;
}
.emoji {
font-size: 4em;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="emoji">🚀</div>
<h1>contribution.usercontent.com</h1>
<p>This domain is coming soon.</p>
</div>
</body>
</html>

BIN
infographic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

51
landing.html Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>contribution.usercontent.com - Coming Soon</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 60px 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
max-width: 600px;
}
h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 20px;
word-break: break-word;
}
p {
color: #666;
font-size: 1.2em;
line-height: 1.6;
}
.emoji {
font-size: 4em;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="emoji">🚀</div>
<h1>contribution.usercontent.com</h1>
<p>This domain is coming soon.</p>
</div>
</body>
</html>

BIN
landing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

51
listings.html Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>contribution.usercontent.com - Coming Soon</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 60px 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
max-width: 600px;
}
h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 20px;
word-break: break-word;
}
p {
color: #666;
font-size: 1.2em;
line-height: 1.6;
}
.emoji {
font-size: 4em;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="emoji">🚀</div>
<h1>contribution.usercontent.com</h1>
<p>This domain is coming soon.</p>
</div>
</body>
</html>

BIN
listings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -1,7 +1,15 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
],
},
};
export default nextConfig;

4349
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,24 @@
"lint": "eslint"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@prisma/adapter-pg": "^7.6.0",
"@prisma/client": "^7.6.0",
"@prisma/config": "^7.6.0",
"@types/pg": "^8.20.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.4.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.7.0",
"next": "16.2.2",
"pg": "^8.20.0",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"shadcn": "^4.1.2",
"tailwind-merge": "^3.5.0",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@@ -22,6 +35,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.2",
"prisma": "^7.6.0",
"tailwindcss": "^4",
"typescript": "^5"
}

View File

@@ -2,7 +2,6 @@ import { defineConfig } from '@prisma/config'
export default defineConfig({
datasource: {
adapter: 'postgresql',
url: "postgres://postgres:fsI47H3bbfpNCQD8GiCre2T97OOY8BwUJ02hUYzZfehYhutUJPO14BZgYsh0z3sG@65.109.236.58:3945/postgres"
}
})

51
services_code.html Normal file
View File

@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>contribution.usercontent.com - Coming Soon</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
padding: 60px 40px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
text-align: center;
max-width: 600px;
}
h1 {
color: #333;
font-size: 2.5em;
margin-bottom: 20px;
word-break: break-word;
}
p {
color: #666;
font-size: 1.2em;
line-height: 1.6;
}
.emoji {
font-size: 4em;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="emoji">🚀</div>
<h1>contribution.usercontent.com</h1>
<p>This domain is coming soon.</p>
</div>
</body>
</html>

BIN
services_screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

18
src/app/(main)/layout.tsx Normal file
View File

@@ -0,0 +1,18 @@
import { Navbar } from "@/components/navbar";
import { Footer } from "@/components/footer";
export default function MainLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<Navbar />
<main className="flex-grow">
{children}
</main>
<Footer />
</>
);
}

12
src/app/(main)/page.tsx Normal file
View File

@@ -0,0 +1,12 @@
import { prisma } from "@/lib/prisma";
import { LandingPage } from "@/components/landing-page";
// Next.js 15+ allows server components by default
export default async function Home() {
const projects = await prisma.project.findMany({
orderBy: { createdAt: 'desc' },
take: 6 // Sadece en güncel 6 projeyi ana sayfada gösterelim
});
return <LandingPage projects={projects} />;
}

View File

@@ -0,0 +1,143 @@
import { prisma } from "@/lib/prisma";
import Image from "next/image";
import { notFound } from "next/navigation";
import { ArrowLeft, Maximize2, Layers, CheckCircle2, Ruler } from "lucide-react";
import Link from "next/link";
export default async function ProjectDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const project = await prisma.project.findUnique({
where: { id }
});
if (!project) {
notFound();
}
return (
<main className="min-h-screen bg-[#F9F9F9] pt-24 md:pt-32 pb-16 md:pb-24 px-5 md:px-12 lg:px-24">
<div className="max-w-7xl mx-auto">
{/* Back Link */}
<Link href="/projeler" className="group inline-flex items-center gap-3 label-editorial text-gray-400 hover:text-[#1A1C1C] transition-colors mb-12">
<ArrowLeft size={16} className="group-hover:-translate-x-1 transition-transform" />
Geri Dön
</Link>
{/* Hero Header */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 md:gap-16 items-end mb-16 md:mb-24">
<div className="lg:col-span-8">
<p className="label-editorial text-[#795900] mb-4">{project.status}</p>
<h1 className="text-[clamp(3rem,8vw,8rem)] font-inter font-black uppercase tracking-tighter leading-[0.9] break-words">
{project.title}
</h1>
</div>
<div className="lg:col-span-4 pb-4">
<div className="flex flex-col gap-6 border-l-2 border-[#1A1C1C] pl-6 md:pl-8">
<div>
<p className="label-editorial text-gray-400">Konum</p>
<p className="text-xl font-bold uppercase">Menteşe, Muğla</p>
</div>
<div>
<p className="label-editorial text-gray-400">Yıl</p>
<p className="text-xl font-bold uppercase">2023 - 2024</p>
</div>
</div>
</div>
</div>
{/* Main Image */}
<div className="relative aspect-[16/9] md:aspect-[21/9] w-full overflow-hidden bg-gray-200 mb-12 md:mb-24 shadow-2xl">
<Image
src={project.images[0] || "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?q=80&w=2070"}
alt={project.title}
fill
className="object-cover"
priority
sizes="100vw"
/>
</div>
{/* Info Grid */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 md:gap-24 mb-20 md:mb-32">
<div className="lg:col-span-4 space-y-12">
<div className="grid grid-cols-2 gap-8">
<div className="bg-white p-6 md:p-8 border-b-4 border-[#FFBF00] shadow-xl">
<Maximize2 className="text-[#FFBF00] mb-4" size={24} />
<p className="label-editorial text-gray-400">Toplam Alan</p>
<p className="text-3xl font-black">{project.m2} m²</p>
</div>
<div className="bg-white p-6 md:p-8 border-b-4 border-[#1A1C1C] shadow-xl">
<Layers className="text-[#1A1C1C] mb-4" size={24} />
<p className="label-editorial text-gray-400">Oda Sayısı</p>
<p className="text-3xl font-black">{project.rooms}</p>
</div>
</div>
<div className="space-y-6">
<h3 className="text-2xl font-black uppercase flex items-center gap-4">
<CheckCircle2 size={24} className="text-[#FFBF00]" />
Mühendislik Kapsamı
</h3>
<ul className="space-y-4 font-manrope text-gray-600">
<li className="flex items-center gap-4">
<span className="w-2 h-2 bg-[#FFBF00]" />
Statik Proje Tasarımı
</li>
<li className="flex items-center gap-4">
<span className="w-2 h-2 bg-[#FFBF00]" />
Temel Güçlendirme Analizi
</li>
<li className="flex items-center gap-4">
<span className="w-2 h-2 bg-[#FFBF00]" />
Müşavirlik & Denetim
</li>
</ul>
</div>
</div>
<div className="lg:col-span-8 flex flex-col justify-between">
<div className="space-y-12">
<div className="flex gap-8 items-start">
<Ruler size={48} className="text-[#FFBF00] opacity-30 shrink-0" />
<div>
<h2 className="text-4xl font-black uppercase mb-8">Mimari Vizyon</h2>
<p className="text-xl leading-relaxed text-gray-500 font-manrope">
{project.description}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4 md:gap-8 pt-8 md:pt-12 border-t border-gray-100">
<div className="relative aspect-square overflow-hidden bg-gray-100">
<Image
src={project.images[1] || project.images[0]}
alt="Detail 1"
fill
className="object-cover grayscale hover:grayscale-0 transition-all duration-700"
sizes="(max-width: 768px) 100vw, 40vw"
/>
</div>
<div className="relative aspect-square overflow-hidden bg-gray-100">
<Image
src={project.images[0]}
alt="Detail 2"
fill
className="object-cover grayscale hover:grayscale-0 transition-all duration-700 rotate-3 scale-110"
sizes="(max-width: 768px) 100vw, 40vw"
/>
</div>
</div>
</div>
</div>
</div>
{/* CTA */}
<section className="bg-[#1A1C1C] p-10 md:p-16 lg:p-24 text-center text-white">
<p className="label-editorial text-[#FFBF00] mb-8">Sıradaki Proje Sizinki Olabilir</p>
<h3 className="text-3xl md:text-6xl font-inter font-black uppercase mb-12">Benzer Bir Vizyonunuz<br />Var mı?</h3>
<a href="/#iletisim" className="inline-block bg-[#FFBF00] text-[#1A1C1C] px-16 py-6 font-bold uppercase hover:bg-white transition-all shadow-2xl">Bize Ulaşın</a>
</section>
</div>
</main>
);
}

View File

@@ -0,0 +1,62 @@
import { prisma } from "@/lib/prisma";
import Image from "next/image";
import { ArrowUpRight } from "lucide-react";
import Link from "next/link";
export default async function ProjelerPage() {
const projects = await prisma.project.findMany({
orderBy: { createdAt: 'desc' }
});
return (
<main className="pt-24 md:pt-32 pb-16 md:pb-24 px-5 md:px-12 lg:px-24 bg-[#F3F3F3] min-h-screen">
<div className="max-w-7xl mx-auto">
<header className="mb-12 md:mb-20">
<p className="label-editorial text-[#795900] mb-4">Portfolyo Koleksiyonu</p>
<h1 className="text-[clamp(3rem,10vw,8rem)] font-inter font-black uppercase tracking-tighter leading-[0.9]">
PROJELER
</h1>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 md:gap-4 px-1 bg-white/5 shadow-2xl">
{projects.map((p, i) => (
<Link
href={`/projeler/${p.id}`}
key={p.id}
className={`group relative h-[400px] md:h-[600px] overflow-hidden ${i % 3 === 0 ? "md:col-span-2" : ""}`}
>
<Image
src={p.images[0] || "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?q=80&w=2070"}
alt={p.title}
fill
className="object-cover grayscale group-hover:grayscale-0 group-hover:scale-105 transition-all duration-1000"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 66vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-[#1A1C1C] via-transparent to-transparent opacity-60 group-hover:opacity-90 transition-opacity" />
<div className="absolute bottom-6 left-6 right-6 md:bottom-12 md:left-12 md:right-12 flex justify-between items-end">
<div>
<div className="label-editorial text-[#FFBF00] mb-2">{p.status}</div>
<h2 className="text-3xl md:text-4xl font-inter font-black text-white uppercase tracking-tighter">{p.title}</h2>
<div className="flex gap-6 mt-4 text-[10px] font-manrope uppercase tracking-widest text-white/60">
<span>{p.m2} m² Alan</span>
<span>Muğla / Menteşe</span>
</div>
</div>
<div className="w-16 h-16 bg-[#FFBF00] text-[#1A1C1C] flex items-center justify-center opacity-0 group-hover:opacity-100 translate-y-10 group-hover:translate-y-0 transition-all">
<ArrowUpRight size={24} />
</div>
</div>
</Link>
))}
</div>
{projects.length === 0 && (
<div className="py-32 text-center">
<p className="label-editorial text-gray-400">Henüz proje eklenmemiş.</p>
</div>
)}
</div>
</main>
);
}

View File

@@ -0,0 +1,113 @@
import { HardHat, Compass, Ruler, Building2, Key, CheckCircle2 } from "lucide-react";
import Image from "next/image";
export default function SurecPage() {
const steps = [
{
id: "01",
title: "Keşif ve Analiz",
desc: "Müşteri beklentileri, arsa ve çevre koşullarının teknik analizi ile sürecin ilk taşını koyuyoruz.",
icon: Compass,
img: "https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2062",
accent: "İhtiyaç Analizi"
},
{
id: "02",
title: "Mühendislik Tasarımı",
desc: "İleri düzey statik hesaplamalar ve 3D modellemelerle yapının teknik iskeletini oluşturuyoruz.",
icon: Ruler,
img: "https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2062",
accent: "Statik Proje"
},
{
id: "03",
title: "Ruhsat ve İzinler",
desc: "Yasal mevzuat ve yerel yönetmeliklere tam uyum için tüm resmi süreçlerin takibi.",
icon: HardHat,
img: "https://images.unsplash.com/photo-1589829545856-d10d557cf95f?q=80&w=2070",
accent: "Belediye Onayı"
},
{
id: "04",
title: "İnşaat Uygulama",
desc: "Kendi ekip ve ekipmanlarımızla, mühendislik denetimi altında titiz inşaat süreci.",
icon: Building2,
img: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=2070",
accent: "Şantiye Yönetimi"
},
{
id: "05",
title: "Teslimat ve Sonuç",
desc: "Sıfır hata prensibi ve tüm testlerin ardından hayallerin anahtarla buluştuğu an.",
icon: Key,
img: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?q=80&w=2070",
accent: "Anahtar Teslim"
}
];
return (
<main className="pt-32 pb-24 bg-[#F9F9F9] min-h-screen">
{/* Hero */}
<section className="px-6 md:px-24 mb-32">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-end gap-12">
<div className="md:w-2/3">
<p className="label-editorial text-[#795900] mb-6 tracking-[0.2em] uppercase font-bold">Mühendislik Disiplini</p>
<h1 className="text-6xl md:text-9xl font-inter font-black uppercase leading-none tracking-tighter">
İnşaat<br />
<span className="text-transparent border-t-2 border-b-2 border-[#1A1C1C] my-4 inline-block py-2">Süreci</span>
</h1>
</div>
<div className="md:w-1/3">
<p className="text-xl text-gray-500 leading-relaxed font-manrope">
Her adımda şeffaflık, mühendislik hassasiyeti ve sarsılmaz bir disiplini. Utku Kırkan ile süreciniz her an kontrol altındadır.
</p>
</div>
</div>
</section>
{/* The Infographic List */}
<section className="px-6 md:px-24">
<div className="max-w-7xl mx-auto space-y-px bg-gray-200 shadow-2xl">
{steps.map((s, i) => (
<div key={s.id} className="group relative bg-[#F9F9F9] flex flex-col lg:flex-row hover:bg-white transition-all duration-700 overflow-hidden min-h-[500px]">
<div className="w-full lg:w-1/3 p-12 lg:p-16 flex flex-col justify-between border-b lg:border-b-0 lg:border-r border-gray-100">
<div>
<span className="text-8xl font-inter font-black text-gray-100 group-hover:text-[#FFBF00]/30 transition-colors duration-500">{s.id}</span>
<p className="label-editorial text-[#795900] mt-8 mb-4">{s.accent}</p>
<h2 className="text-4xl font-inter font-black uppercase tracking-tighter leading-none mb-6 group-hover:translate-x-3 transition-transform duration-500">{s.title}</h2>
</div>
<div className="w-16 h-16 bg-[#1A1C1C] text-white flex items-center justify-center rounded-sm">
<s.icon size={24} />
</div>
</div>
<div className="w-full lg:w-1/3 p-12 lg:p-16 flex items-center bg-[#F3F3F3]">
<p className="text-lg text-gray-500 leading-relaxed font-manrope group-hover:text-[#1A1C1C] transition-colors">{s.desc}</p>
</div>
<div className="w-full lg:w-1/3 relative h-[300px] lg:h-auto overflow-hidden grayscale group-hover:grayscale-0 transition-all duration-1000">
<div className="absolute inset-0 bg-[#795900]/20 mix-blend-multiply opacity-0 group-hover:opacity-40 transition-opacity" />
<Image
src={s.img}
alt={s.title}
fill
className="object-cover transition-transform duration-1000 group-hover:scale-110"
sizes="(max-width: 1024px) 100vw, 33vw"
/>
</div>
</div>
))}
</div>
</section>
{/* Call to Action */}
<section className="mt-32 px-6 md:px-24">
<div className="max-w-7xl mx-auto bg-[#1A1C1C] p-16 md:p-24 text-center">
<CheckCircle2 size={48} className="mx-auto text-[#FFBF00] mb-10" />
<h3 className="text-4xl md:text-6xl font-inter font-black text-white uppercase mb-8">Hayalleriniz<br />Güvenli Ellerde</h3>
<a href="/#iletisim" className="inline-block bg-[#FFBF00] text-[#1A1C1C] px-12 py-5 font-bold uppercase tracking-widest hover:bg-white transition-all shadow-xl">Hemen Başlayalım</a>
</div>
</section>
</main>
);
}

32
src/app/actions.ts Normal file
View File

@@ -0,0 +1,32 @@
import { prisma } from "@/lib/prisma";
import { revalidatePath } from "next/cache";
export async function createProject(formData: FormData) {
"use server";
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const m2 = parseInt(formData.get("m2") as string);
const rooms = formData.get("rooms") as string;
const status = formData.get("status") as string;
const imageUrl = formData.get("imageUrl") as string;
await prisma.project.create({
data: {
title,
description,
m2,
rooms,
status,
images: imageUrl ? [imageUrl] : []
},
});
revalidatePath("/admin");
revalidatePath("/");
}
export async function deleteProject(id: string) {
"use server";
await prisma.project.delete({ where: { id } });
revalidatePath("/admin");
}

129
src/app/admin/page.tsx Normal file
View File

@@ -0,0 +1,129 @@
import { prisma } from "@/lib/prisma";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { deleteProject, createProject } from "@/app/actions";
import { Plus, Trash2, MapPin, Square, Image as ImageIcon } from "lucide-react";
import Image from "next/image";
export default async function AdminPage() {
const projects = await prisma.project.findMany({
orderBy: { createdAt: 'desc' }
});
return (
<div className="p-8 bg-[#111827] min-h-screen text-gray-100 font-sans">
<div className="max-w-7xl mx-auto">
<div className="flex justify-between items-center mb-12 border-b border-gray-800 pb-8">
<div>
<h1 className="text-4xl font-heading font-bold text-white uppercase tracking-tight">
PROJE <span className="text-[#F59E0B]">YÖNETİMİ</span>
</h1>
<p className="text-gray-400 mt-2">Utku Kırkan Portfolyo Yönetim Paneli</p>
</div>
<Dialog>
<DialogTrigger render={<Button className="bg-[#F59E0B] hover:bg-[#d98c0a] text-[#111827] font-bold py-6 px-6 rounded-none uppercase gap-2"><Plus size={20}/> Yeni Proje Ekle</Button>} />
<DialogContent className="bg-[#1f2937] border-gray-700 text-white p-8 rounded-none max-w-2xl">
<DialogHeader className="mb-6">
<DialogTitle className="text-3xl font-heading uppercase">Yeni Proje Girişi</DialogTitle>
</DialogHeader>
<form action={createProject} className="space-y-6">
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-gray-500 tracking-widest">Proje Adı</label>
<Input name="title" placeholder="Örn: Kırkan Villaları" className="bg-[#111827] border-gray-700 rounded-none h-12" required />
</div>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-gray-500 tracking-widest">ıklama</label>
<Textarea name="description" placeholder="Proje detayları..." className="bg-[#111827] border-gray-700 rounded-none min-h-[100px]" required />
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-gray-500 tracking-widest">Toplam m²</label>
<Input name="m2" type="number" placeholder="250" className="bg-[#111827] border-gray-700 rounded-none h-12" required />
</div>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-gray-500 tracking-widest">Oda Sayısı</label>
<Input name="rooms" placeholder="4+1" className="bg-[#111827] border-gray-700 rounded-none h-12" required />
</div>
</div>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-gray-500 tracking-widest">Durum</label>
<Input name="status" placeholder="Tamamlandı / Devam Ediyor" className="bg-[#111827] border-gray-700 rounded-none h-12" required />
</div>
<div className="space-y-2">
<label className="text-xs uppercase font-bold text-gray-500 tracking-widest">Görsel URL (Unsplash vb.)</label>
<Input name="imageUrl" placeholder="https://..." className="bg-[#111827] border-gray-700 rounded-none h-12" />
</div>
<Button type="submit" className="w-full bg-[#F59E0B] hover:bg-[#d98c0a] text-[#111827] font-bold py-6 rounded-none uppercase text-lg mt-4 transition-all">
Projeyi Kaydet
</Button>
</form>
</DialogContent>
</Dialog>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{projects.map((p) => (
<Card key={p.id} className="bg-[#1f2937] border-gray-800 rounded-none shadow-2xl group">
<div className="relative h-48 w-full bg-gray-800 overflow-hidden">
{p.images[0] ? (
<Image
src={p.images[0]}
alt={p.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-500"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
) : (
<div className="flex items-center justify-center h-full text-gray-600">
<ImageIcon size={48} />
</div>
)}
<div className="absolute top-4 right-4">
<form action={deleteProject.bind(null, p.id)}>
<Button variant="destructive" size="icon" className="h-10 w-10 bg-red-600/20 hover:bg-red-600 border border-red-600 text-white rounded-none transition-all">
<Trash2 size={18} />
</Button>
</form>
</div>
</div>
<CardHeader className="pb-2 text-center uppercase tracking-tighter">
<CardTitle className="text-xl font-heading text-white">{p.title}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center text-sm border-y border-gray-700 py-3 mt-2">
<div className="flex items-center gap-2 text-gray-400">
<Square size={14} className="text-[#F59E0B]"/> {p.m2} m²
</div>
<div className="flex items-center gap-2 text-gray-400">
<Plus size={14} className="text-[#F59E0B]"/> {p.rooms} Oda
</div>
</div>
<div className="flex items-center gap-2 text-[#F59E0B] text-xs font-bold uppercase tracking-widest">
<MapPin size={14} /> {p.status}
</div>
<p className="text-gray-400 text-sm line-clamp-2 italic">"{p.description}"</p>
</CardContent>
</Card>
))}
{projects.length === 0 && (
<div className="col-span-full py-20 text-center bg-[#1f2937] border-2 border-dashed border-gray-700 grayscale">
<ImageIcon className="mx-auto text-gray-700 mb-4" size={64}/>
<h2 className="text-xl text-gray-500 font-heading uppercase">Henüz Proje Eklenmemiş</h2>
<p className="text-gray-600">Başlamak için sağ üstten yeni bir proje girişi yapın.</p>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -1,26 +1,82 @@
@import "tailwindcss";
@source "../../src/**/*.{ts,tsx}";
:root {
--background: #ffffff;
--foreground: #171717;
@theme {
--color-background: #F9F9F9;
--color-foreground: #1A1C1C;
--color-card: #FFFFFF;
--color-card-foreground: #1A1C1C;
--color-popover: #FFFFFF;
--color-popover-foreground: #1A1C1C;
--color-primary: #5F5E5E;
--color-primary-foreground: #FFFFFF;
--color-secondary: #F3F3F3;
--color-secondary-foreground: #1A1C1C;
--color-muted: #F3F3F3;
--color-muted-foreground: #6D6D6D;
--color-accent: #FFBF00;
--color-accent-foreground: #1A1C1C;
--color-destructive: #EE4444;
--color-destructive-foreground: #F9F9F9;
--color-border: rgba(0, 0, 0, 0.05);
--color-input: rgba(0, 0, 0, 0.05);
--color-ring: #FFBF00;
--font-inter: var(--inter-font), sans-serif;
--font-manrope: var(--manrope-font), sans-serif;
--font-bebas-neue: var(--bebas-font), sans-serif;
--radius-sm: 0.125rem;
--radius-md: 0.25rem;
--radius-lg: 0.5rem;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--background: #F9F9F9;
--foreground: #1A1C1C;
--primary: #5F5E5E;
--accent: #FFBF00;
}
* {
border-color: var(--color-border);
}
body {
background-color: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-manrope), sans-serif;
-webkit-font-smoothing: antialiased;
min-height: 100vh;
margin: 0;
overflow-x: hidden;
width: 100%;
position: relative;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.label-editorial {
font-family: var(--font-manrope), sans-serif;
text-transform: uppercase;
letter-spacing: 0.15em;
font-weight: 600;
font-size: 0.75rem;
}
.glass-nav {
background-color: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
}

View File

@@ -1,20 +1,27 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Bebas_Neue, Inter, Manrope } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
const bebasNeue = Bebas_Neue({
weight: "400",
variable: "--bebas-font",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const inter = Inter({
variable: "--inter-font",
subsets: ["latin"],
weight: ["700", "800", "900"],
});
const manrope = Manrope({
variable: "--manrope-font",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Utku Kırkan | Engineering Excellence",
description: "Modern Mühendislik, Güçlü Yapılar. Muğla'da Güvenilir Mühendislik ve İnşaat Hizmetleri.",
};
export default function RootLayout({
@@ -24,10 +31,13 @@ export default function RootLayout({
}>) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
lang="tr"
className={`${bebasNeue.variable} ${inter.variable} ${manrope.variable} h-full`}
style={{ scrollBehavior: 'smooth' }}
>
<body className="min-h-full flex flex-col">{children}</body>
<body className="min-h-full flex flex-col font-sans antialiased text-foreground bg-background">
{children}
</body>
</html>
);
}

View File

@@ -1,65 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}

44
src/components/footer.tsx Normal file
View File

@@ -0,0 +1,44 @@
"use client";
import { motion } from "framer-motion";
export function Footer() {
return (
<footer className="bg-[#0A0B0B] text-white py-24 px-6 md:px-24">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row justify-between gap-20">
<div>
<div className="font-inter font-black text-3xl md:text-4xl uppercase mb-6 md:mb-8 tracking-tighter">
UTKU <span className="text-[#FFBF00]">KIRKAN</span>
</div>
<p className="text-gray-500 max-w-sm font-manrope">
Muğla genelinde modern mühendislik standartlarını estetik tasarım ile buluşturan küratörlüğünü üstlendiğimiz projelerle geleceği inşa ediyoruz.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-12 sm:gap-20">
<div className="space-y-6">
<p className="label-editorial text-[#FFBF00]">Harita</p>
<div className="text-sm font-manrope text-gray-400 space-y-2">
<p>Muğla, Menteşe</p>
<p>Kötekli Mevki</p>
<p>Türkiye</p>
</div>
</div>
<div className="space-y-6">
<p className="label-editorial text-[#FFBF00]">Sosyal</p>
<div className="text-sm font-manrope text-gray-400 space-y-2">
<a href="#" className="block hover:text-white transition-colors uppercase tracking-widest">INSTAGRAM</a>
<a href="#" className="block hover:text-white transition-colors uppercase tracking-widest">LINKEDIN</a>
</div>
</div>
</div>
</div>
<div className="max-w-7xl mx-auto mt-16 md:mt-24 pt-12 border-t border-white/5 flex flex-col md:flex-row justify-between text-[10px] text-gray-600 tracking-widest uppercase gap-8">
<p>© {new Date().getFullYear()} UTKU KIRKAN ENGINEERING EXCELLENCE.</p>
<div className="flex flex-wrap gap-x-8 gap-y-4">
<span>Gizlilik Politikası</span>
<span>KVKK Aydınlatma Metni</span>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,289 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import {
Phone,
MessageSquare,
ArrowUpRight
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import Link from "next/link";
const fadeInUp = {
initial: { opacity: 0, y: 30 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true },
transition: { duration: 0.8, ease: "easeOut" as const }
};
interface Project {
id: string;
title: string;
description: string;
m2: number;
rooms: string;
images: string[];
status: string;
}
export function LandingPage({ projects }: { projects: Project[] }) {
return (
<div className="flex flex-col min-h-screen bg-[#F9F9F9] text-[#1A1C1C] selection:bg-[#FFBF00] selection:text-[#1A1C1C]">
{/* Hero Section - The Curated Monolith */}
<section className="relative h-[100vh] min-h-[700px] flex items-center overflow-hidden bg-[#1A1C1C]">
<div className="absolute inset-0 z-0 h-full w-full">
<Image
src="https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=2070"
alt="Architecture"
fill
className="object-cover opacity-40 grayscale"
priority
sizes="100vw"
/>
<div className="absolute inset-0 bg-gradient-to-r from-[#1A1C1C] via-[#1A1C1C]/40 to-transparent" />
</div>
<div className="relative z-10 px-5 md:px-12 lg:px-24 w-full max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 1 }}
>
<p className="label-editorial text-[#FFBF00] mb-6 whitespace-normal max-w-[280px] sm:max-w-none leading-relaxed">Muğla Mühendislik & Müteahhitlik</p>
<h1 className="text-[clamp(2.5rem,8vw,8rem)] font-inter font-black text-white leading-[0.95] uppercase mb-8 break-words hyphens-auto overflow-hidden">
Modern<br />
<span className="text-white border-t border-b border-white/20 block sm:inline-block py-2 md:py-4 my-2 font-inter tracking-tighter w-fit">Mühendislik</span><br />
Güçlü<br />
Yapılar
</h1>
<div className="flex flex-col sm:flex-row gap-6 mt-12">
<a href="/projeler">
<Button className="bg-[#FFBF00] hover:bg-[#E6AC00] text-[#1A1C1C] font-bold text-lg px-10 py-8 rounded-sm uppercase transition-all shadow-2xl">
Projeleri İncele
</Button>
</a>
<a href="/surec" className="flex items-center gap-4 group">
<div className="w-16 h-16 rounded-full border border-white/20 flex items-center justify-center text-white group-hover:bg-white group-hover:text-[#1A1C1C] transition-all">
<ArrowUpRight size={24} />
</div>
<span className="label-editorial text-white">Süreç</span>
</a>
</div>
</motion.div>
</div>
</section>
{/* Services Section - Architectural Excellence */}
<section id="hizmetler" className="py-20 md:py-32 bg-[#F9F9F9] px-5 md:px-12 lg:px-24">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-24 items-start px-0">
<motion.div {...fadeInUp} className="lg:col-span-5 lg:sticky top-32 z-10 bg-inherit">
<p className="label-editorial text-[#795900] mb-4">Uzmanlık Alanlarımız</p>
<h2 className="text-[clamp(2rem,8vw,4.5rem)] font-inter font-black uppercase leading-[1.1] mb-6 md:mb-8 break-words overflow-hidden">
Mimari<br />
<span className="bg-[#FFBF00] px-3 py-1 text-white inline-block mt-2 text-[0.8em] sm:text-[1em] max-w-full truncate sm:overflow-visible">Mükemmeliyet</span>
</h2>
<p className="text-lg text-gray-600 leading-relaxed max-w-sm">
Sadece bina inşa etmiyoruz; mühendislik disipliniyle şekillendirilmiş, Muğla'nın dokusuna uyumlu yaşam alanları tasarlıyoruz.
</p>
</motion.div>
<div className="lg:col-span-7 space-y-1 relative z-0">
{[
{
id: "01",
title: "Statik Proje Tasarımı",
desc: "Maksimum dayanıklılık için ileri düzey mühendislik hesaplamaları ve deprem yönetmeliğine tam uyumlu taşıyıcı sistem analizleri.",
img: "https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2062"
},
{
id: "02",
title: "Müteahhitlik Hizmetleri",
desc: "A'dan Z'ye anahtar teslim inşaat süreçleri. Kaliteli malzeme ve usta işçiliği mühendislik denetimiyle birleştiriyoruz.",
img: "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=2070"
},
{
id: "03",
title: "Teknik Danışmanlık",
desc: "İnşaat yatırım süreçlerinizde risk yönetimi, bütçe optimizasyonu ve yasal mevzuat uyumu konusunda profesyonel destek.",
img: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?q=80&w=1974"
}
].map((s, i) => (
<motion.div
key={i}
{...fadeInUp}
transition={{ delay: i * 0.2 }}
className="group relative bg-[#F3F3F3] p-8 md:p-12 overflow-hidden hover:bg-[#FFFFFF] transition-all duration-500"
>
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-8 relative z-10">
<div className="flex gap-8 items-start">
<span className="text-4xl font-inter font-bold text-[#FFBF00]/30">{s.id}</span>
<div>
<h3 className="text-3xl font-inter font-black uppercase mb-4 group-hover:text-[#795900] transition-colors">{s.title}</h3>
<p className="text-gray-500 max-w-md group-hover:text-[#1A1C1C] transition-colors">{s.desc}</p>
</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-500 w-32 h-32 relative hidden md:block">
<Image src={s.img} alt={s.title} fill className="object-cover grayscale" sizes="128px" />
</div>
</div>
</motion.div>
))}
</div>
</div>
</div>
</section>
{/* Projects Section - Horizontal Monolith */}
<section id="projeler" className="py-20 md:py-32 bg-[#FFFFFF] px-5 md:px-12 lg:px-24 overflow-hidden">
<div className="max-w-7xl mx-auto">
<motion.div {...fadeInUp} className="mb-20 flex flex-col md:flex-row justify-between items-end gap-10">
<h2 className="text-[clamp(2.5rem,8vw,8rem)] font-inter font-black uppercase leading-[0.9] tracking-tighter break-words">
PROJELER
</h2>
<div className="w-full md:w-1/3 h-px bg-gray-200 hidden md:block" />
<p className="label-editorial text-gray-400">2023 - 2024 Koleksiyonu</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-12">
{projects.length > 0 ? projects.map((p, i) => (
<motion.div
key={p.id}
{...fadeInUp}
transition={{ delay: i * 0.1 }}
className="group cursor-pointer"
>
<Link href={`/projeler/${p.id}`}>
<div className="relative aspect-[4/5] overflow-hidden bg-gray-100 mb-8">
<Image
src={p.images[0] || "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?q=80&w=2070"}
alt={p.title}
fill
className="object-cover grayscale translate-y-3 group-hover:translate-y-0 group-hover:grayscale-0 transition-all duration-700"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
<div className="absolute inset-0 bg-[#795900]/20 opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="absolute bottom-8 right-8 w-16 h-16 bg-[#1A1C1C] flex items-center justify-center text-[#FFBF00] translate-y-20 group-hover:translate-y-0 transition-transform duration-500">
<ArrowUpRight size={24} />
</div>
</div>
<p className="label-editorial text-[#795900] mb-2">{p.status}</p>
<h4 className="text-3xl font-inter font-black uppercase mb-4">{p.title}</h4>
<div className="flex gap-6 text-[10px] font-manrope uppercase tracking-widest text-gray-400">
<span>{p.m2} m²</span>
<span>Muğla, Menteşe</span>
</div>
</Link>
</motion.div>
)) : (
[
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?q=80&w=2070",
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=2070",
"https://images.unsplash.com/photo-1497366754035-f200968a6e72?q=80&w=1974"
].map((url, i) => (
<div key={i} className="aspect-[4/5] bg-gray-100 relative grayscale opacity-50 overflow-hidden" tabIndex={0}>
<Image src={url} alt="Fallback" fill className="object-cover" sizes="(max-width: 768px) 100vw, 33vw" />
</div>
))
)}
</div>
</div>
</section>
{/* About Section */}
<section id="hakkimizda" className="py-20 md:py-32 bg-[#F3F3F3] px-5 md:px-12 lg:px-24">
<div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-12 md:gap-24 items-center">
<motion.div {...fadeInUp} className="relative aspect-square bg-[#FFFFFF] p-4 shadow-2xl">
<div className="relative w-full h-full overflow-hidden">
<Image
src="https://images.unsplash.com/photo-1497366754035-f200968a6e72?q=80&w=1974"
alt="Office"
fill
className="object-cover grayscale"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
</div>
<div className="absolute -bottom-6 -left-6 md:-bottom-10 md:-left-10 bg-[#FFBF00] p-8 md:p-12 text-[#1A1C1C] shadow-xl">
<span className="text-5xl md:text-7xl font-inter font-black">10+</span>
<p className="label-editorial text-xs md:text-base">Yıllık Güven</p>
</div>
</motion.div>
<motion.div {...fadeInUp}>
<p className="label-editorial text-[#795900] mb-8">Kurumsal Vizyon</p>
<h2 className="text-4xl md:text-7xl font-inter font-black uppercase leading-none mb-8 md:mb-12">
Disiplinli<br />
İnşaat Kültürü
</h2>
<div className="space-y-8 text-gray-600 text-lg leading-relaxed font-manrope">
<p>
İnşaat mühendisliği her şeyden önce bir disiplin, yapı güvenliği ise bir haktır. Utku Kırkan olarak, Muğla'nın mimari zenginliğini koruyarak geleceğin sağlam temellerini bugün atıyoruz.
</p>
<p>
Analitik çözümleme ve estetik kaygıyı bir dengede tutarak, "The Curated Monolith" anlayışıyla zamansız yapılar inşa ediyoruz. Her detayda titizlik, her projede sürdürebilirlik sözü veriyoruz.
</p>
</div>
</motion.div>
</div>
</section>
{/* Contact Section */}
<section id="iletisim" className="py-20 md:py-32 bg-[#1A1C1C] text-white px-5 md:px-12 lg:px-24">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 md:gap-32">
<motion.div {...fadeInUp}>
<p className="label-editorial text-[#FFBF00] mb-8">İletişim & Randevu</p>
<h2 className="text-5xl md:text-8xl font-inter font-black uppercase leading-[0.9] mb-8 md:mb-12">
Projeyi<br />
Konuşalım
</h2>
<div className="space-y-12">
<div className="flex items-center gap-8 group cursor-pointer">
<div className="w-20 h-20 border border-white/20 flex items-center justify-center text-[#FFBF00] group-hover:bg-[#FFBF00] group-hover:text-[#1A1C1C] transition-all duration-500">
<Phone size={32} />
</div>
<div>
<p className="label-editorial text-gray-500">Telefon Hattı</p>
<p className="text-3xl font-inter font-bold">+90 5XX XXX XX XX</p>
</div>
</div>
<div className="flex items-center gap-8 group cursor-pointer">
<div className="w-20 h-20 border border-white/20 flex items-center justify-center text-[#FFBF00] group-hover:bg-[#FFBF00] group-hover:text-[#1A1C1C] transition-all duration-500">
<MessageSquare size={32} />
</div>
<div>
<p className="label-editorial text-gray-500">WhatsApp</p>
<a href="https://wa.me/905XXXXXXXXX" className="text-3xl font-inter font-bold border-b-2 border-transparent hover:border-[#FFBF00] transition-all">MESAJ GÖNDER</a>
</div>
</div>
</div>
</motion.div>
<motion.div {...fadeInUp} className="bg-[#FFFFFF] p-8 md:p-12 lg:p-16 text-[#1A1C1C]">
<h3 className="text-3xl font-inter font-black uppercase mb-12">Ücretsiz Teklif Formu</h3>
<form className="space-y-8">
<div className="space-y-2 group">
<label className="label-editorial text-gray-400 group-focus-within:text-[#FFBF00] transition-colors">Ad Soyad</label>
<Input className="border-0 border-b border-gray-200 rounded-none bg-transparent px-0 focus-visible:ring-0 focus-visible:border-[#FFBF00] h-12 transition-all" placeholder="ADINIZ SOYADINIZ" />
</div>
<div className="space-y-2 group">
<label className="label-editorial text-gray-400 group-focus-within:text-[#FFBF00] transition-colors">Tel / E-posta</label>
<Input className="border-0 border-b border-gray-200 rounded-none bg-transparent px-0 focus-visible:ring-0 focus-visible:border-[#FFBF00] h-12 transition-all" placeholder="iletisim@bilgi.com" />
</div>
<div className="space-y-2 group">
<label className="label-editorial text-gray-400 group-focus-within:text-[#FFBF00] transition-colors">Proje Detayları</label>
<Textarea className="border-0 border-b border-gray-200 rounded-none bg-transparent px-0 focus-visible:ring-0 focus-visible:border-[#FFBF00] min-h-[120px] resize-none transition-all" placeholder="PROJENİZ HAKKINDA KISA BİLGİ..." />
</div>
<Button className="w-full bg-[#1A1C1C] hover:bg-[#795900] text-white font-bold h-20 rounded-none uppercase transition-all shadow-xl group">
Formu Gönder <ArrowUpRight className="ml-4 group-hover:translate-x-2 group-hover:-translate-y-2 transition-transform" />
</Button>
</form>
</motion.div>
</div>
</div>
</section>
</div>
);
}

78
src/components/navbar.tsx Normal file
View File

@@ -0,0 +1,78 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { ArrowUpRight, Menu, X } from "lucide-react";
import { useState } from "react";
import Link from "next/link";
export function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const navLinks = [
{ name: "Hizmetler", href: "/#hizmetler" },
{ name: "Projeler", href: "/projeler" },
{ name: "Süreç", href: "/surec" },
{ name: "Vizyon", href: "/#hakkimizda" },
];
return (
<>
<nav className="fixed top-0 w-full z-[100] glass-nav h-20 flex items-center px-6 md:px-12 justify-between">
<div className="font-inter font-black text-xl md:text-2xl tracking-tighter uppercase relative z-[101]">
<Link href="/">Utku <span className="text-[#795900]">Kırkan</span></Link>
</div>
{/* Desktop Menu */}
<div className="hidden md:flex items-center gap-10 label-editorial">
{navLinks.map((link) => (
<Link key={link.name} href={link.href} className="hover:text-[#795900] transition-colors">
{link.name}
</Link>
))}
<Link href="/#iletisim" className="bg-[#1A1C1C] text-white px-6 py-2 hover:bg-[#795900] transition-all font-inter font-bold uppercase text-xs tracking-widest">
İletişim
</Link>
</div>
{/* Mobile Toggle */}
<button
className="md:hidden relative z-[101] p-2 text-[#1A1C1C]"
onClick={() => setIsOpen(!isOpen)}
aria-label="Toggle Menu"
>
{isOpen ? <X size={28} /> : <Menu size={28} />}
</button>
</nav>
{/* Mobile Menu Overlay */}
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="fixed inset-0 z-[99] bg-white flex flex-col items-center justify-center gap-8 md:hidden"
>
{navLinks.map((link) => (
<Link
key={link.name}
href={link.href}
onClick={() => setIsOpen(false)}
className="text-4xl font-inter font-black uppercase tracking-tighter hover:text-[#795900] transition-colors"
>
{link.name}
</Link>
))}
<Link
href="/#iletisim"
onClick={() => setIsOpen(false)}
className="mt-4 text-4xl font-inter font-black uppercase tracking-tighter text-[#795900]"
>
İLETİŞİM
</Link>
</motion.div>
)}
</AnimatePresence>
</>
);
}

View File

@@ -0,0 +1,60 @@
"use client"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

103
src/components/ui/card.tsx Normal file
View File

@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { XIcon } from "lucide-react"
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: DialogPrimitive.Backdrop.Props) {
return (
<DialogPrimitive.Backdrop
data-slot="dialog-overlay"
className={cn(
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: DialogPrimitive.Popup.Props & {
showCloseButton?: boolean
}) {
return (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Popup
data-slot="dialog-content"
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
render={
<Button
variant="ghost"
className="absolute top-2 right-2"
size="icon-sm"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Popup>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close render={<Button variant="outline" />}>
Close
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn(
"font-heading text-base leading-none font-medium",
className
)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: DialogPrimitive.Description.Props) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn(
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
className
)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,201 @@
"use client"
import * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "@/lib/utils"
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
const Select = SelectPrimitive.Root
function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1 p-1", className)}
{...props}
/>
)
}
function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
return (
<SelectPrimitive.Value
data-slot="select-value"
className={cn("flex flex-1 text-left", className)}
{...props}
/>
)
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon
render={
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
}
/>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
side = "bottom",
sideOffset = 4,
align = "center",
alignOffset = 0,
alignItemWithTrigger = true,
...props
}: SelectPrimitive.Popup.Props &
Pick<
SelectPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Positioner
side={side}
sideOffset={sideOffset}
align={align}
alignOffset={alignOffset}
alignItemWithTrigger={alignItemWithTrigger}
className="isolate z-50"
>
<SelectPrimitive.Popup
data-slot="select-content"
data-align-trigger={alignItemWithTrigger}
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.List>{children}</SelectPrimitive.List>
<SelectScrollDownButton />
</SelectPrimitive.Popup>
</SelectPrimitive.Positioner>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: SelectPrimitive.GroupLabel.Props) {
return (
<SelectPrimitive.GroupLabel
data-slot="select-label"
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: SelectPrimitive.Item.Props) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap">
{children}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator
render={
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
}
>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: SelectPrimitive.Separator.Props) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) {
return (
<SelectPrimitive.ScrollUpArrow
data-slot="select-scroll-up-button"
className={cn(
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronUpIcon
/>
</SelectPrimitive.ScrollUpArrow>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) {
return (
<SelectPrimitive.ScrollDownArrow
data-slot="select-scroll-down-button"
className={cn(
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<ChevronDownIcon
/>
</SelectPrimitive.ScrollDownArrow>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@@ -0,0 +1,25 @@
"use client"
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
...props
}: SeparatorPrimitive.Props) {
return (
<SeparatorPrimitive
data-slot="separator"
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props}
/>
)
}
export { Separator }

116
src/components/ui/table.tsx Normal file
View File

@@ -0,0 +1,116 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Textarea }

29
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,29 @@
import { PrismaClient } from "../generated/prisma";
import { PrismaPg } from "@prisma/adapter-pg";
import pg from "pg";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
const connectionString = process.env.DATABASE_URL;
// During Next.js build phase (NEXT_PHASE), we allow the connection string to be missing
// to prevent the build from failing. In a real runtime, the Prisma client will
// throw an error if the connection fails, which is the expected behavior.
if (!connectionString && process.env.NODE_ENV === "production" && !process.env.NEXT_PHASE) {
throw new Error("DATABASE_URL is not set in the environment.");
}
const getAdapter = () => {
if (!connectionString) return undefined;
const pool = new pg.Pool({ connectionString });
return new PrismaPg(pool);
};
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
adapter: getAdapter(),
log: ["query"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

71
src/seed.ts Normal file
View File

@@ -0,0 +1,71 @@
import { PrismaClient } from "./generated/prisma";
import { PrismaPg } from "@prisma/adapter-pg";
import pg from "pg";
import * as dotenv from "dotenv";
dotenv.config();
const connectionString = process.env.DATABASE_URL;
if (!connectionString) throw new Error("DATABASE_URL is not set");
const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
async function main() {
console.log("Seeding projects...");
const projects = [
{
title: "VILLA AURELIAN",
description: "Menteşe'nin kalbinde, modern mimariyle doğanın buluştuğu lüks villa projesi. Brütalist dokunuşlar ve geniş cam yüzeyler ile ferah bir yaşam alanı.",
m2: 450,
rooms: "5+1",
status: "TAMAMLANDI",
images: [
"https://images.unsplash.com/photo-1600585154340-be6161a56a0c?q=80&w=2070",
"https://images.unsplash.com/photo-1600596542815-ffad4c1539a9?q=80&w=2075"
]
},
{
title: "ZENITH APARTMANI",
description: "Şehir merkezinde yüksek mühendislik standartlarıyla inşa edilen, deprem güvenliği öncelikli modern konut projesi.",
m2: 1200,
rooms: "3+1 / 4+1",
status: "DEVAM EDİYOR",
images: [
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=2070",
"https://images.unsplash.com/photo-1497366754035-f200968a6e72?q=80&w=1974"
]
},
{
title: "MİLAS REZİDANS",
description: "Muğla'nın tarihi dokusuna uyumlu, sürdürülebilir enerji çözümleriyle donatılmış ekolojik konut kompleksi.",
m2: 3200,
rooms: "Stüdyo / 2+1",
status: "PLANLANIYOR",
images: [
"https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2062",
"https://images.unsplash.com/photo-1503387762-592deb58ef4e?q=80&w=2062"
]
}
];
for (const project of projects) {
await prisma.project.create({
data: project
});
}
console.log("Seeding complete!");
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
await pool.end();
});