This commit is contained in:
Şahan Hasret
2025-11-21 17:46:30 +03:00
parent c0b7fb463e
commit 76c31274d5
46 changed files with 3675 additions and 1043 deletions

2
.gitignore vendored
View File

@@ -39,3 +39,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
/lib/generated/prisma

36
ADMIN_CREDENTIALS.md Normal file
View File

@@ -0,0 +1,36 @@
# Admin Panel Giriş Bilgileri
## Yönetim Paneli URL
`http://localhost:3000/yonetim-paneli-a2m-secure`
## Giriş Bilgileri
**Kullanıcı Adı:** `admin`
**Şifre:** `AnkaraBel!2025.A2`
---
## Önemli Notlar
- Bu şifre güçlü bir kombinasyondur (büyük/küçük harf, rakam, özel karakterler)
- Production ortamına geçerken mutlaka .env dosyasındaki `ADMIN_DEFAULT_PASSWORD` değişkenini değiştirin
- Şifre bcrypt ile hashlenmiş olarak veritabanında saklanır
## Veritabanı Bilgileri
- **Database:** SQLite (`prisma/dev.db`)
- **ORM:** Prisma 7.0.0
- **Auth:** JWT + bcrypt + httpOnly cookies
## Seed Komutu
Veritabanını sıfırlamak için:
```bash
npm run db:reset
```
Veritabanı içeriğini kontrol için:
```bash
sqlite3 prisma/dev.db "SELECT * FROM User;"
```

View File

@@ -0,0 +1,73 @@
import { NextRequest, NextResponse } from 'next/server';
import { compare } from 'bcryptjs';
import { prisma } from '@/lib/prisma';
import { encrypt } from '@/lib/auth';
export async function POST(request: NextRequest) {
try {
const { username, password } = await request.json();
if (!username || !password) {
return NextResponse.json(
{ error: 'Kullanıcı adı ve şifre gerekli' },
{ status: 400 }
);
}
// Kullanıcıyı bul
const user = await prisma.user.findUnique({
where: { username },
});
if (!user) {
return NextResponse.json(
{ error: 'Kullanıcı adı veya şifre hatalı' },
{ status: 401 }
);
}
// Şifreyi kontrol et
const isPasswordValid = await compare(password, user.password);
if (!isPasswordValid) {
return NextResponse.json(
{ error: 'Kullanıcı adı veya şifre hatalı' },
{ status: 401 }
);
}
// JWT token oluştur
const token = await encrypt({
userId: user.id.toString(),
username: user.username,
});
// Response oluştur
const response = NextResponse.json(
{
user: {
id: user.id,
username: user.username,
},
},
{ status: 200 }
);
// Cookie'ye token ekle
response.cookies.set('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24, // 24 saat
path: '/',
});
return response;
} catch (error) {
console.error('Login error:', error);
return NextResponse.json(
{ error: 'Giriş yapılırken bir hata oluştu' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,13 @@
import { NextResponse } from 'next/server';
export async function POST() {
const response = NextResponse.json(
{ message: ıkış başarılı' },
{ status: 200 }
);
// Cookie'yi sil
response.cookies.delete('session');
return response;
}

View File

@@ -0,0 +1,18 @@
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
export async function GET() {
const session = await getSession();
if (!session) {
return NextResponse.json({ authenticated: false }, { status: 401 });
}
return NextResponse.json({
authenticated: true,
user: {
id: session.userId,
username: session.username,
},
});
}

103
app/api/cameras/route.ts Normal file
View File

@@ -0,0 +1,103 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm kameraları getir
export async function GET() {
try {
const cameras = await prisma.camera.findMany({
orderBy: { order: 'asc' },
});
return NextResponse.json(cameras);
} catch (error) {
console.error('Cameras fetch error:', error);
return NextResponse.json(
{ error: 'Kameralar alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Yeni kamera ekle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const camera = await prisma.camera.create({
data: {
name: data.name,
location: data.location,
videoUrl: data.videoUrl,
status: data.status,
viewers: data.viewers || 0,
order: data.order,
},
});
return NextResponse.json(camera, { status: 201 });
} catch (error) {
console.error('Camera create error:', error);
return NextResponse.json(
{ error: 'Kamera oluşturulurken hata oluştu' },
{ status: 500 }
);
}
});
}
// PUT - Kamera güncelle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const camera = await prisma.camera.update({
where: { id: data.id },
data: {
name: data.name,
location: data.location,
videoUrl: data.videoUrl,
status: data.status,
viewers: data.viewers,
order: data.order,
},
});
return NextResponse.json(camera);
} catch (error) {
console.error('Camera update error:', error);
return NextResponse.json(
{ error: 'Kamera güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - Kamera sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.camera.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'Kamera silindi' });
} catch (error) {
console.error('Camera delete error:', error);
return NextResponse.json(
{ error: 'Kamera silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

116
app/api/documents/route.ts Normal file
View File

@@ -0,0 +1,116 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm dokümanları getir
export async function GET() {
try {
const documents = await prisma.document.findMany({
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(documents);
} catch (error) {
console.error('Documents fetch error:', error);
return NextResponse.json(
{ error: 'Dökümanlar alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Yeni döküman ekle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const document = await prisma.document.create({
data: {
title: data.title,
type: data.type,
category: data.category,
downloadUrl: data.fileUrl || data.downloadUrl,
size: data.fileSize || data.size || '0 MB',
description: data.description,
date: new Date().toISOString().split('T')[0],
},
});
return NextResponse.json(document, { status: 201 });
} catch (error) {
console.error('Document create error:', error);
return NextResponse.json(
{ error: 'Döküman oluşturulurken hata oluştu' },
{ status: 500 }
);
}
});
}
// PUT - Döküman güncelle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const document = await prisma.document.update({
where: { id: data.id },
data: {
title: data.title,
type: data.type,
category: data.category,
downloadUrl: data.fileUrl || data.downloadUrl,
size: data.fileSize || data.size,
description: data.description,
},
});
return NextResponse.json(document);
} catch (error) {
console.error('Document update error:', error);
return NextResponse.json(
{ error: 'Döküman güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - Döküman sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
console.log('DELETE document - ID:', id);
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.document.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'Döküman silindi' });
} catch (error: unknown) {
console.error('Document delete error:', error);
// Prisma P2025: Record not found
if (error && typeof error === 'object' && 'code' in error && error.code === 'P2025') {
return NextResponse.json(
{ error: 'Döküman bulunamadı (zaten silinmiş olabilir)' },
{ status: 404 }
);
}
return NextResponse.json(
{ error: 'Döküman silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

97
app/api/faqs/route.ts Normal file
View File

@@ -0,0 +1,97 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm SSS'leri getir
export async function GET() {
try {
const faqs = await prisma.fAQ.findMany({
orderBy: { order: 'asc' },
});
return NextResponse.json(faqs);
} catch (error) {
console.error('FAQs fetch error:', error);
return NextResponse.json(
{ error: 'SSS alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Yeni SSS ekle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const faq = await prisma.fAQ.create({
data: {
question: data.question,
answer: data.answer,
order: data.order,
},
});
return NextResponse.json(faq, { status: 201 });
} catch (error) {
console.error('FAQ create error:', error);
return NextResponse.json(
{ error: 'SSS oluşturulurken hata oluştu' },
{ status: 500 }
);
}
});
}
// PUT - SSS güncelle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const faq = await prisma.fAQ.update({
where: { id: data.id },
data: {
question: data.question,
answer: data.answer,
order: data.order,
},
});
return NextResponse.json(faq);
} catch (error) {
console.error('FAQ update error:', error);
return NextResponse.json(
{ error: 'SSS güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - SSS sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.fAQ.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'SSS silindi' });
} catch (error) {
console.error('FAQ delete error:', error);
return NextResponse.json(
{ error: 'SSS silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

View File

@@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Aktif canlı yayın bilgisini getir
export async function GET() {
try {
const liveStream = await prisma.liveStream.findFirst({
where: { active: true },
});
return NextResponse.json(liveStream || { active: false, url: '', title: '' });
} catch (error) {
console.error('Live stream fetch error:', error);
return NextResponse.json(
{ error: 'Canlı yayın bilgisi alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Canlı yayın ayarlarını güncelle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
// Önce tüm canlı yayınları pasif yap
await prisma.liveStream.updateMany({
data: { active: false },
});
// Yeni ayarları oluştur veya güncelle
const liveStream = await prisma.liveStream.upsert({
where: { id: data.id || 0 },
update: {
url: data.url,
active: data.active,
title: data.title,
},
create: {
url: data.url,
active: data.active,
title: data.title,
},
});
return NextResponse.json(liveStream);
} catch (error) {
console.error('Live stream update error:', error);
return NextResponse.json(
{ error: 'Canlı yayın güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}

102
app/api/media/route.ts Normal file
View File

@@ -0,0 +1,102 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm medya öğelerini getir
export async function GET() {
try {
const media = await prisma.media.findMany({
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(media);
} catch (error) {
console.error('Media fetch error:', error);
return NextResponse.json(
{ error: 'Medya öğeleri alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Yeni medya öğesi ekle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const media = await prisma.media.create({
data: {
title: data.title,
type: data.type,
thumbnail: data.thumbnail,
videoUrl: data.videoUrl,
description: data.description,
date: new Date().toISOString().split('T')[0],
},
});
return NextResponse.json(media, { status: 201 });
} catch (error) {
console.error('Media create error:', error);
return NextResponse.json(
{ error: 'Medya öğesi oluşturulurken hata oluştu' },
{ status: 500 }
);
}
});
}
// PUT - Medya öğesi güncelle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const media = await prisma.media.update({
where: { id: data.id },
data: {
title: data.title,
type: data.type,
thumbnail: data.thumbnail,
videoUrl: data.videoUrl,
description: data.description,
},
});
return NextResponse.json(media);
} catch (error) {
console.error('Media update error:', error);
return NextResponse.json(
{ error: 'Medya öğesi güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - Medya öğesi sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.media.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'Medya öğesi silindi' });
} catch (error) {
console.error('Media delete error:', error);
return NextResponse.json(
{ error: 'Medya öğesi silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

98
app/api/messages/route.ts Normal file
View File

@@ -0,0 +1,98 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm mesajları getir (Auth gerekli)
export async function GET(request: NextRequest) {
return withAuth(request, async () => {
try {
const messages = await prisma.message.findMany({
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(messages);
} catch (error) {
console.error('Messages fetch error:', error);
return NextResponse.json(
{ error: 'Mesajlar alınırken hata oluştu' },
{ status: 500 }
);
}
});
}
// POST - Yeni mesaj gönder (Public - iletişim formu)
export async function POST(request: NextRequest) {
try {
const data = await request.json();
const message = await prisma.message.create({
data: {
name: data.name,
email: data.email,
phone: data.phone || '',
subject: data.subject,
type: data.type || 'contact',
message: data.message,
read: false,
date: new Date().toISOString().split('T')[0],
},
});
return NextResponse.json(message, { status: 201 });
} catch (error) {
console.error('Message create error:', error);
return NextResponse.json(
{ error: 'Mesaj gönderilirken hata oluştu' },
{ status: 500 }
);
}
}
// PUT - Mesajı okundu olarak işaretle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const message = await prisma.message.update({
where: { id: data.id },
data: { read: data.read },
});
return NextResponse.json(message);
} catch (error) {
console.error('Message update error:', error);
return NextResponse.json(
{ error: 'Mesaj güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - Mesaj sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.message.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'Mesaj silindi' });
} catch (error) {
console.error('Message delete error:', error);
return NextResponse.json(
{ error: 'Mesaj silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

107
app/api/news/route.ts Normal file
View File

@@ -0,0 +1,107 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm haberleri getir
export async function GET() {
try {
const news = await prisma.news.findMany({
orderBy: { createdAt: 'desc' },
});
return NextResponse.json(news);
} catch (error) {
console.error('News fetch error:', error);
return NextResponse.json(
{ error: 'Haberler alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Yeni haber ekle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const news = await prisma.news.create({
data: {
title: data.title,
summary: data.summary || data.content.substring(0, 150) + '...',
content: data.content,
category: data.category,
image: data.image,
author: data.author,
tags: data.tags || '',
date: new Date().toISOString().split('T')[0],
featured: data.featured || false,
},
});
return NextResponse.json(news, { status: 201 });
} catch (error) {
console.error('News create error:', error);
return NextResponse.json(
{ error: 'Haber oluşturulurken hata oluştu' },
{ status: 500 }
);
}
});
}
// PUT - Haber güncelle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const news = await prisma.news.update({
where: { id: data.id },
data: {
title: data.title,
summary: data.summary,
content: data.content,
category: data.category,
image: data.image,
author: data.author,
tags: data.tags,
},
});
return NextResponse.json(news);
} catch (error) {
console.error('News update error:', error);
return NextResponse.json(
{ error: 'Haber güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - Haber sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.news.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'Haber silindi' });
} catch (error) {
console.error('News delete error:', error);
return NextResponse.json(
{ error: 'Haber silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

52
app/api/settings/route.ts Normal file
View File

@@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Site ayarlarını getir
export async function GET() {
try {
const settings = await prisma.siteSettings.findFirst({
where: { key: 'main' },
});
if (!settings) {
return NextResponse.json({ error: 'Ayarlar bulunamadı' }, { status: 404 });
}
return NextResponse.json(JSON.parse(settings.value));
} catch (error) {
console.error('Settings fetch error:', error);
return NextResponse.json(
{ error: 'Ayarlar alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Site ayarlarını güncelle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const settings = await prisma.siteSettings.upsert({
where: { key: 'main' },
update: {
value: JSON.stringify(data),
},
create: {
key: 'main',
value: JSON.stringify(data),
},
});
return NextResponse.json(JSON.parse(settings.value));
} catch (error) {
console.error('Settings update error:', error);
return NextResponse.json(
{ error: 'Ayarlar güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}

101
app/api/slider/route.ts Normal file
View File

@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm slider item'ları getir
export async function GET() {
try {
const items = await prisma.sliderItem.findMany({
orderBy: { id: 'asc' },
});
return NextResponse.json(items);
} catch (error) {
console.error('Slider items fetch error:', error);
return NextResponse.json(
{ error: 'Slider items alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Yeni slider item ekle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const item = await prisma.sliderItem.create({
data: {
title: data.title,
description: data.description,
buttonText: data.buttonText,
buttonLink: data.buttonLink,
active: data.active ?? true,
},
});
return NextResponse.json(item, { status: 201 });
} catch (error) {
console.error('Slider item create error:', error);
return NextResponse.json(
{ error: 'Slider item oluşturulurken hata oluştu' },
{ status: 500 }
);
}
});
}
// PUT - Slider item güncelle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const item = await prisma.sliderItem.update({
where: { id: data.id },
data: {
title: data.title,
description: data.description,
buttonText: data.buttonText,
buttonLink: data.buttonLink,
active: data.active,
},
});
return NextResponse.json(item);
} catch (error) {
console.error('Slider item update error:', error);
return NextResponse.json(
{ error: 'Slider item güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - Slider item sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.sliderItem.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'Slider item silindi' });
} catch (error) {
console.error('Slider item delete error:', error);
return NextResponse.json(
{ error: 'Slider item silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

101
app/api/stations/route.ts Normal file
View File

@@ -0,0 +1,101 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { withAuth } from '@/lib/auth';
// GET - Tüm metro istasyonlarını getir
export async function GET() {
try {
const stations = await prisma.metroStation.findMany({
orderBy: { order: 'asc' },
});
return NextResponse.json(stations);
} catch (error) {
console.error('Stations fetch error:', error);
return NextResponse.json(
{ error: 'İstasyonlar alınırken hata oluştu' },
{ status: 500 }
);
}
}
// POST - Yeni istasyon ekle (Auth gerekli)
export async function POST(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const station = await prisma.metroStation.create({
data: {
name: data.name,
status: data.status,
progress: data.progress,
order: data.order,
description: data.description || data.estimatedCompletion,
},
});
return NextResponse.json(station, { status: 201 });
} catch (error) {
console.error('Station create error:', error);
return NextResponse.json(
{ error: 'İstasyon oluşturulurken hata oluştu' },
{ status: 500 }
);
}
});
}
// PUT - İstasyon güncelle (Auth gerekli)
export async function PUT(request: NextRequest) {
return withAuth(request, async () => {
try {
const data = await request.json();
const station = await prisma.metroStation.update({
where: { id: data.id },
data: {
name: data.name,
status: data.status,
progress: data.progress,
order: data.order,
description: data.description || data.estimatedCompletion,
},
});
return NextResponse.json(station);
} catch (error) {
console.error('Station update error:', error);
return NextResponse.json(
{ error: 'İstasyon güncellenirken hata oluştu' },
{ status: 500 }
);
}
});
}
// DELETE - İstasyon sil (Auth gerekli)
export async function DELETE(request: NextRequest) {
return withAuth(request, async () => {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json({ error: 'ID gerekli' }, { status: 400 });
}
await prisma.metroStation.delete({
where: { id: parseInt(id) },
});
return NextResponse.json({ message: 'İstasyon silindi' });
} catch (error) {
console.error('Station delete error:', error);
return NextResponse.json(
{ error: 'İstasyon silinirken hata oluştu' },
{ status: 500 }
);
}
});
}

View File

@@ -11,7 +11,7 @@ export default function Documents() {
const [selectedCategory, setSelectedCategory] = useState('all');
useEffect(() => {
setDocuments(dataStore.getDocuments());
dataStore.getDocuments().then(setDocuments);
}, []);
const categories = [

View File

@@ -11,13 +11,15 @@ export default function LiveStream() {
const [cameras, setCameras] = useState<Camera[]>([]);
useEffect(() => {
const loadedCameras = dataStore.getCameras()
.filter(cam => cam.status === 'online')
.sort((a, b) => a.order - b.order);
setCameras(loadedCameras);
if (loadedCameras.length > 0) {
setSelectedCamera(loadedCameras[0].id);
}
dataStore.getCameras().then(loadedCameras => {
const onlineCameras = loadedCameras
.filter(cam => cam.status === 'online')
.sort((a, b) => a.order - b.order);
setCameras(onlineCameras);
if (onlineCameras.length > 0) {
setSelectedCamera(onlineCameras[0].id);
}
});
}, []);
const selectedCam = cameras.find(cam => cam.id === selectedCamera) || cameras[0];

View File

@@ -13,7 +13,7 @@ export default function News() {
const [selectedNews, setSelectedNews] = useState<number | null>(null);
useEffect(() => {
setNewsData(dataStore.getNews());
dataStore.getNews().then(setNewsData);
}, []);
const filteredNews = selectedCategory === 'all'

View File

@@ -7,11 +7,51 @@ import { dataStore, type SiteSettings } from '@/lib/dataStore';
export default function Contact() {
const [settings, setSettings] = useState<SiteSettings | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
subject: '',
message: ''
});
useEffect(() => {
setSettings(dataStore.getSiteSettings());
dataStore.getSiteSettings().then(setSettings);
}, []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
try {
await dataStore.addMessage({
name: formData.name,
email: formData.email,
phone: formData.phone,
subject: formData.subject,
message: formData.message,
type: 'contact'
});
alert('Mesajınız başarıyla gönderildi! En kısa sürede size dönüş yapacağız.');
// Formu temizle
setFormData({
name: '',
email: '',
phone: '',
subject: '',
message: ''
});
} catch (error) {
console.error('Mesaj gönderilirken hata:', error);
alert('Mesaj gönderilirken bir hata oluştu. Lütfen tekrar deneyin.');
} finally {
setIsSubmitting(false);
}
};
if (!settings) return null;
return (
@@ -152,10 +192,7 @@ export default function Contact() {
</p>
</div>
<form className="max-w-2xl mx-auto space-y-6" onSubmit={(e) => {
e.preventDefault();
alert('Mesajınız gönderildi! (Demo)');
}}>
<form className="max-w-2xl mx-auto space-y-6" onSubmit={handleSubmit}>
{/* İki Sütunlu Alan */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Ad Soyad */}
@@ -167,6 +204,8 @@ export default function Contact() {
type="text"
id="contact-name"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00B4D8] focus:border-transparent outline-none transition-all text-gray-900 placeholder:text-gray-500"
placeholder="Adınız ve Soyadınız"
/>
@@ -181,6 +220,8 @@ export default function Contact() {
type="email"
id="contact-email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00B4D8] focus:border-transparent outline-none transition-all text-gray-900 placeholder:text-gray-500"
placeholder="ornek@email.com"
/>
@@ -194,6 +235,8 @@ export default function Contact() {
<input
type="tel"
id="contact-phone"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00B4D8] focus:border-transparent outline-none transition-all text-gray-900 placeholder:text-gray-500"
placeholder="0(5__) ___ __ __"
/>
@@ -208,6 +251,8 @@ export default function Contact() {
type="text"
id="contact-subject"
required
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00B4D8] focus:border-transparent outline-none transition-all text-gray-900 placeholder:text-gray-500"
placeholder="Mesaj konusu"
/>
@@ -223,6 +268,8 @@ export default function Contact() {
id="contact-message"
required
rows={6}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00B4D8] focus:border-transparent outline-none transition-all resize-none text-gray-900 placeholder:text-gray-500"
placeholder="Lütfen mesajınızı yazınız..."
></textarea>
@@ -246,12 +293,25 @@ export default function Contact() {
<div className="text-center pt-4">
<button
type="submit"
className="px-8 py-3 bg-[#00B4D8] text-white rounded-lg hover:bg-[#004B87] transition-colors font-semibold shadow-lg hover:shadow-xl flex items-center justify-center space-x-2 mx-auto"
disabled={isSubmitting}
className="px-8 py-3 bg-[#00B4D8] text-white rounded-lg hover:bg-[#004B87] transition-colors font-semibold shadow-lg hover:shadow-xl flex items-center justify-center space-x-2 mx-auto disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
<span>Gönder</span>
{isSubmitting ? (
<>
<svg className="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>Gönderiliyor...</span>
</>
) : (
<>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
<span>Gönder</span>
</>
)}
</button>
</div>
</form>

View File

@@ -13,7 +13,7 @@ export default function MediaGallery() {
const [selectedMedia, setSelectedMedia] = useState<number | null>(null);
useEffect(() => {
setMediaItems(dataStore.getMedia());
dataStore.getMedia().then(setMediaItems);
}, []);
const filteredMedia = selectedTab === 'all'

View File

@@ -10,7 +10,8 @@ import DocumentsSection from "@/components/DocumentsSection";
import MediaGallery from "@/components/MediaGallery";
import ComplaintForm from "@/components/ComplaintForm";
import ContactSection from "@/components/ContactSection";
import { dataStore } from '@/lib/dataStore';
import { dataStore, type SliderItem, type LiveStreamConfig } from '@/lib/dataStore';
import type { NewsItem } from '@/data/news';
export default function Home() {
const [showLiveStream, setShowLiveStream] = useState(false);
@@ -23,15 +24,15 @@ export default function Home() {
const [currentSlide, setCurrentSlide] = useState(0);
// Gerçek veriler için state
const [heroSlides, setHeroSlides] = useState(dataStore.getSlider());
const [newsData, setNewsData] = useState(dataStore.getNews());
const [liveStreamConfig, setLiveStreamConfig] = useState(dataStore.getLiveStream());
const [heroSlides, setHeroSlides] = useState<SliderItem[]>([]);
const [newsData, setNewsData] = useState<NewsItem[]>([]);
const [liveStreamConfig, setLiveStreamConfig] = useState<LiveStreamConfig>({ url: '', active: false });
// Verileri yükle
useEffect(() => {
setHeroSlides(dataStore.getSlider());
setNewsData(dataStore.getNews());
setLiveStreamConfig(dataStore.getLiveStream());
dataStore.getSlider().then(setHeroSlides);
dataStore.getNews().then(setNewsData);
dataStore.getLiveStream().then(setLiveStreamConfig);
}, []);
// Modal açıldığında yukarı kaydır - KALDIRILDI (kullanıcı deneyimi için)

View File

@@ -11,7 +11,9 @@ export default function FAQPage() {
const [faqs, setFaqs] = useState<FAQ[]>([]);
useEffect(() => {
setFaqs(dataStore.getFAQs().sort((a, b) => a.order - b.order));
dataStore.getFAQs().then(faqs => {
setFaqs(faqs.sort((a, b) => a.order - b.order));
});
}, []);
const toggleFAQ = (id: number) => {

File diff suppressed because it is too large Load Diff

View File

@@ -14,12 +14,28 @@ export default function AdminLogin() {
setLoading(true);
setError('');
// Basit auth - production'da backend ile yapılmalı
if (credentials.username === 'admin' && credentials.password === 'A2Metro2025!') {
localStorage.setItem('admin_token', 'authenticated');
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
});
const data = await response.json();
if (!response.ok) {
setError(data.error || 'Giriş başarısız');
setLoading(false);
return;
}
// Başarılı giriş
router.push('/yonetim-paneli-a2m-secure/dashboard');
} else {
setError('Kullanıcı adı veya şifre hatalı');
} catch (error) {
console.error('Login error:', error);
setError('Bir hata oluştu. Lütfen tekrar deneyin.');
setLoading(false);
}
};

View File

@@ -11,7 +11,7 @@ export default function ContactSection({ onClose }: ContactSectionProps) {
const [settings, setSettings] = useState<SiteSettings | null>(null);
useEffect(() => {
setSettings(dataStore.getSiteSettings());
dataStore.getSiteSettings().then(setSettings);
}, []);
if (!settings) return null;

View File

@@ -28,7 +28,7 @@ export default function DocumentsSection({ onClose }: DocumentsSectionProps) {
];
useEffect(() => {
setDocuments(dataStore.getDocuments());
dataStore.getDocuments().then(setDocuments);
}, []);
const filteredDocs = selectedCategory === 'all'

View File

@@ -9,7 +9,7 @@ export default function Footer() {
const [settings, setSettings] = useState<SiteSettings | null>(null);
useEffect(() => {
setSettings(dataStore.getSiteSettings());
dataStore.getSiteSettings().then(setSettings);
}, []);
if (!settings) return null;
@@ -153,6 +153,20 @@ export default function Footer() {
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.899 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.899-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06l.045.03zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z"/>
</svg>
</a>
{/* YouTube */}
{settings.social.youtube && (
<a
href={settings.social.youtube}
target="_blank"
rel="noopener noreferrer"
className="w-9 h-9 rounded-full bg-[#00B4D8] hover:bg-[#48CAE4] flex items-center justify-center transition-colors"
aria-label="YouTube"
>
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
</a>
)}
{/* LinkedIn */}
{settings.social.linkedin && (
<a
@@ -175,7 +189,7 @@ export default function Footer() {
{/* Alt Bilgi */}
<div className="border-t border-white/10 mt-8 pt-8 text-center">
<p className="text-gray-400 text-sm">
© {new Date().getFullYear()} {settings.companyInfo.fullName} - Tüm hakları saklıdır.
© {new Date().getFullYear()} {settings.company.name} - Tüm hakları saklıdır.
</p>
</div>
</div>

View File

@@ -8,7 +8,7 @@ export default function Header() {
const [settings, setSettings] = useState<SiteSettings | null>(null);
useEffect(() => {
setSettings(dataStore.getSiteSettings());
dataStore.getSiteSettings().then(setSettings);
}, []);
const menuItems = [
@@ -151,6 +151,18 @@ export default function Header() {
</svg>
</a>
)}
{settings.social.youtube && (
<a
href={settings.social.youtube}
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 rounded-lg bg-white/10 hover:bg-white/20 flex items-center justify-center transition-all hover:scale-110"
>
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
</a>
)}
{settings.social.linkedin && (
<a
href={settings.social.linkedin}

View File

@@ -15,7 +15,7 @@ export default function MediaGallery({ onClose }: MediaGalleryProps) {
const [selectedMedia, setSelectedMedia] = useState<number | null>(null);
useEffect(() => {
setMediaItems(dataStore.getMedia());
dataStore.getMedia().then(setMediaItems);
}, []);
const filteredMedia = selectedMediaTab === 'all'

View File

@@ -10,34 +10,36 @@ export default function MetroLine() {
const [currentStationIndex, setCurrentStationIndex] = useState(0);
useEffect(() => {
// Metro istasyonlarını yükle
const loadedStations = dataStore.getMetroStations();
setStations(loadedStations);
// Metro istasyonlarını yükle (async)
dataStore.getMetroStations().then(loadedStations => {
setStations(loadedStations);
// Admin panelinden seçili istasyonu al
const adminSelectedId = localStorage.getItem('metro_selected_station');
if (adminSelectedId) {
setSelectedStationId(parseInt(adminSelectedId));
// Seçili istasyonun index'ini bul
const selectedIndex = loadedStations.findIndex(s => s.id === parseInt(adminSelectedId));
if (selectedIndex !== -1) {
// Metro animasyonu başlat
let currentIndex = 0;
const interval = setInterval(() => {
if (currentIndex < selectedIndex) {
currentIndex++;
setCurrentStationIndex(currentIndex);
} else {
clearInterval(interval);
}
}, 2000); // Her 2 saniyede bir ilerle
// Admin panelinden seçili istasyonu al
const adminSelectedId = localStorage.getItem('metro_selected_station');
if (adminSelectedId) {
setSelectedStationId(parseInt(adminSelectedId));
// Seçili istasyonun index'ini bul
const selectedIndex = loadedStations.findIndex(s => s.id === parseInt(adminSelectedId));
if (selectedIndex !== -1) {
// Metro animasyonu başlat
let currentIndex = 0;
const interval = setInterval(() => {
if (currentIndex < selectedIndex) {
currentIndex++;
setCurrentStationIndex(currentIndex);
} else {
clearInterval(interval);
}
}, 2000); // Her 2 saniyede bir ilerle
return () => clearInterval(interval);
return () => clearInterval(interval);
}
}
}
});
}, []);
const getStationStyle = (index: number, station: MetroStation) => {
// Seçili istasyonun index'ini bul
const selectedIndex = selectedStationId

View File

@@ -10,135 +10,5 @@ export interface Document {
featured?: boolean;
}
export const documentsData: Document[] = [
{
id: 1,
title: "A2 Metro Hattı İhale Şartnamesi",
category: 'ihale',
date: "15 Ocak 2025",
size: "2.4 MB",
type: "PDF",
description: "Metro inşaatı için teknik şartname ve ihale koşulları",
featured: true
},
{
id: 2,
title: "Proje Teknik Çizimleri",
category: 'teknik',
date: "10 Şubat 2025",
size: "15.8 MB",
type: "DWG",
description: "Tüm istasyonların teknik mimari çizimleri",
featured: true
},
{
id: 3,
title: "Çevresel Etki Değerlendirme Raporu",
category: 'cevresel',
date: "5 Mart 2025",
size: "4.2 MB",
type: "PDF",
description: "Proje çevresel etki analizi ve önlemler"
},
{
id: 4,
title: "2025 Ekim Ayı İlerleme Raporu",
category: 'raporlar',
date: "1 Kasım 2025",
size: "1.8 MB",
type: "PDF",
description: "Aylık proje ilerleme ve istatistikler",
featured: true
},
{
id: 5,
title: "Güvenlik Planı ve Prosedürleri",
category: 'guvenlik',
date: "20 Ocak 2025",
size: "3.1 MB",
type: "PDF",
description: "İnşaat sahası güvenlik protokolleri"
},
{
id: 6,
title: "İstasyon Tasarım Detayları",
category: 'teknik',
date: "8 Şubat 2025",
size: "8.5 MB",
type: "PDF",
description: "İstasyon iç ve dış mekan tasarım detayları"
},
{
id: 7,
title: "Finansman Planı",
category: 'ihale',
date: "12 Ocak 2025",
size: "1.2 MB",
type: "XLSX",
description: "Proje bütçe ve finansman dağılımı"
},
{
id: 8,
title: "2025 Eylül Ayı İlerleme Raporu",
category: 'raporlar',
date: "1 Ekim 2025",
size: "2.1 MB",
type: "PDF",
description: "Aylık proje ilerleme ve istatistikler"
},
{
id: 9,
title: "Gürültü ve Titreşim Analizi",
category: 'cevresel',
date: "18 Mart 2025",
size: "3.8 MB",
type: "PDF",
description: "Çevre gürültü ve titreşim ölçüm sonuçları"
},
{
id: 10,
title: "İş Güvenliği Risk Analizi",
category: 'guvenlik',
date: "5 Şubat 2025",
size: "2.7 MB",
type: "PDF",
description: "İnşaat sahası risk değerlendirme raporu"
},
{
id: 11,
title: "Elektrik ve Mekanik Sistem Şartnamesi",
category: 'teknik',
date: "15 Mart 2025",
size: "6.3 MB",
type: "PDF",
description: "Metro elektrik ve mekanik sistemler teknik şartnamesi"
},
{
id: 12,
title: "Su Yönetimi Planı",
category: 'cevresel',
date: "10 Nisan 2025",
size: "2.2 MB",
type: "PDF",
description: "İnşaat sahası su kullanımı ve yönetimi"
}
];
export const documentCategories = [
{ id: 'all', name: 'Tümü', icon: '📋' },
{ id: 'ihale', name: 'İhale Belgeleri', icon: '📄' },
{ id: 'teknik', name: 'Teknik Dökümanlar', icon: '📐' },
{ id: 'cevresel', name: 'Çevresel Etki', icon: '🌱' },
{ id: 'raporlar', name: 'İlerleme Raporları', icon: '📊' },
{ id: 'guvenlik', name: 'Güvenlik Belgeleri', icon: '🛡️' },
];
export const getFileIcon = (type: string) => {
switch (type) {
case 'PDF': return '📕';
case 'DWG': return '📐';
case 'XLSX': return '📊';
case 'DOCX': return '📄';
default: return '📄';
}
};
// Mock data removed - all data comes from database
export const documentsData: Document[] = [];

View File

@@ -11,105 +11,5 @@ export interface MediaItem {
featured?: boolean;
}
export const mediaData: MediaItem[] = [
{
id: 1,
type: 'video',
title: 'A2 Metro Hattı Genel Tanıtım',
thumbnail: 'https://images.pexels.com/photos/17152223/pexels-photo-17152223.jpeg',
videoUrl: 'https://www.youtube.com/embed/b9q88QDEcKg',
date: '20 Ekim 2025',
duration: '5:32',
description: 'A2 Metro Hattı projesinin genel tanıtımı ve istasyonların detayları',
category: 'Tanıtım',
featured: true
},
{
id: 2,
type: 'photo',
title: 'Dikimevi İstasyonu İnşaat Çalışmaları',
thumbnail: 'https://images.pexels.com/photos/17302615/pexels-photo-17302615.jpeg',
date: '18 Ekim 2025',
description: 'Dikimevi metro istasyonunda devam eden kazı ve inşaat çalışmaları',
category: 'İnşaat',
featured: true
},
{
id: 3,
type: 'photo',
title: 'Tuzluçayır İstasyonu Temel Atma',
thumbnail: 'https://images.pexels.com/photos/33950678/pexels-photo-33950678.jpeg',
date: '15 Ekim 2025',
description: 'Tuzluçayır istasyonunun temel atma töreni anları',
category: 'Etkinlik'
},
{
id: 4,
type: 'video',
title: 'Metro İnşaatı İlerleme Raporu',
thumbnail: 'https://images.pexels.com/photos/253647/pexels-photo-253647.jpeg',
videoUrl: 'https://www.youtube.com/embed/b9q88QDEcKg',
date: '12 Ekim 2025',
duration: '8:15',
description: 'Ekim ayı metro inşaatı ilerleme raporu ve gelecek hedefler',
category: 'Rapor'
},
{
id: 5,
type: 'photo',
title: 'Modern İstasyon Tasarımları',
thumbnail: 'https://images.pexels.com/photos/17152223/pexels-photo-17152223.jpeg',
date: '10 Ekim 2025',
description: 'Yeni nesil metro istasyonlarının modern iç mekan tasarımları',
category: 'Tasarım'
},
{
id: 6,
type: 'video',
title: 'Çevre Dostu Metro Projesi',
thumbnail: 'https://images.pexels.com/photos/17302615/pexels-photo-17302615.jpeg',
videoUrl: 'https://www.youtube.com/embed/b9q88QDEcKg',
date: '8 Ekim 2025',
duration: '6:45',
description: 'Metro projesinde kullanılan çevre dostu teknolojiler ve sürdürülebilir yaklaşımlar',
category: 'Çevre'
},
{
id: 7,
type: 'photo',
title: 'İşçi Güvenliği Eğitimi',
thumbnail: 'https://images.pexels.com/photos/33950678/pexels-photo-33950678.jpeg',
date: '5 Ekim 2025',
description: 'İnşaat sahalarında iş güvenliği eğitimleri',
category: 'Güvenlik'
},
{
id: 8,
type: 'photo',
title: 'Ray Döşeme Çalışmaları',
thumbnail: 'https://images.pexels.com/photos/253647/pexels-photo-253647.jpeg',
date: '1 Ekim 2025',
description: 'Metro hattında ray döşeme işlemlerinin başlaması',
category: 'İnşaat'
},
{
id: 9,
type: 'video',
title: 'TBM Makinesi Tünel Kazısı',
thumbnail: 'https://images.pexels.com/photos/17152223/pexels-photo-17152223.jpeg',
videoUrl: 'https://www.youtube.com/embed/b9q88QDEcKg',
date: '28 Eylül 2025',
duration: '4:20',
description: 'Dev tünel açma makinesinin çalışma anları',
category: 'Teknoloji'
},
{
id: 10,
type: 'photo',
title: 'Gece Vardiyası Çalışmaları',
thumbnail: 'https://images.pexels.com/photos/17302615/pexels-photo-17302615.jpeg',
date: '25 Eylül 2025',
description: 'İnşaat sahalarında 7/24 devam eden çalışmalar',
category: 'İnşaat'
}
];
// Mock data removed - all data comes from database
export const mediaData: MediaItem[] = [];

View File

@@ -12,121 +12,5 @@ export interface MetroStation {
image?: string;
}
export const metroStations: MetroStation[] = [
{
id: 1,
name: 'ABİDİN PAŞA',
status: 'completed',
progress: 100,
startDate: '10 Ocak 2023',
expectedCompletion: '30 Aralık 2023',
actualCompletion: '25 Aralık 2023',
connections: ['M1'],
features: ['Aktarma İstasyonu', 'Otopark', 'Engelsiz Erişim', 'Ticari Alanlar'],
dailyCapacity: '45,000'
},
{
id: 2,
name: 'AŞIK VEYSEL',
status: 'completed',
progress: 100,
startDate: '15 Şubat 2023',
expectedCompletion: '15 Mart 2024',
actualCompletion: '10 Mart 2024',
connections: [],
features: ['Modern Tasarım', 'Güvenlik Sistemleri', 'Engelsiz Erişim'],
dailyCapacity: '35,000'
},
{
id: 3,
name: 'TUZLUÇAYIR',
status: 'in-progress',
progress: 75,
startDate: '1 Mart 2024',
expectedCompletion: '30 Haziran 2025',
connections: [],
features: ['Modern Tasarım', 'Güneş Enerjisi', 'Güvenlik Sistemleri', 'Engelsiz Erişim'],
dailyCapacity: '40,000'
},
{
id: 4,
name: 'GENERAL ZEKİ DOĞAN',
status: 'in-progress',
progress: 45,
startDate: '15 Nisan 2024',
expectedCompletion: '30 Eylül 2025',
connections: [],
features: ['Otopark', 'Bisiklet Park Alanı', 'Güvenlik Sistemleri'],
dailyCapacity: '30,000'
},
{
id: 5,
name: 'FAHRİ KORUTÜRK',
status: 'planned',
progress: 15,
startDate: '1 Haziran 2024',
expectedCompletion: '31 Aralık 2025',
connections: [],
features: ['Modern Mimari', 'Yeşil Alan', 'Engelsiz Erişim'],
dailyCapacity: '35,000'
},
{
id: 6,
name: 'CENGİZHAN',
status: 'planned',
progress: 10,
startDate: '1 Ağustos 2024',
expectedCompletion: '28 Şubat 2026',
connections: [],
features: ['Ticari Alanlar', 'Otopark', 'Güvenlik Sistemleri'],
dailyCapacity: '32,000'
},
{
id: 7,
name: 'AKŞEMSETTİN',
status: 'planned',
progress: 5,
startDate: '1 Ekim 2024',
expectedCompletion: '30 Nisan 2026',
connections: [],
features: ['Modern Tasarım', 'Engelsiz Erişim', 'Bisiklet Park Alanı'],
dailyCapacity: '28,000'
},
{
id: 8,
name: 'NATOYOLU',
status: 'planned',
progress: 5,
startDate: '1 Aralık 2024',
expectedCompletion: '30 Haziran 2026',
connections: ['M2'],
features: ['Aktarma İstasyonu', 'Otopark', 'Ticari Alanlar', 'Modern Tasarım'],
dailyCapacity: '50,000'
}
];
export const getStatusColor = (status: MetroStation['status']) => {
switch (status) {
case 'completed':
return 'bg-green-500';
case 'in-progress':
return 'bg-blue-500';
case 'planned':
return 'bg-yellow-500';
default:
return 'bg-gray-500';
}
};
export const getStatusText = (status: MetroStation['status']) => {
switch (status) {
case 'completed':
return 'Tamamlandı';
case 'in-progress':
return 'Devam Ediyor';
case 'planned':
return 'Planlanıyor';
default:
return 'Bilinmiyor';
}
};
// Mock data removed - all data comes from database
export const metroStations: MetroStation[] = [];

View File

@@ -8,103 +8,11 @@ export interface NewsItem {
author: string;
content: string;
tags: string[];
featured?: boolean; // Ana sayfada gösterilecek mi
featured?: boolean;
}
export const newsData: NewsItem[] = [
{
id: 1,
category: 'construction',
title: 'A2 Metro Hattı Kazı Çalışmaları Hızla İlerliyor',
summary: 'Dikimevi-Natoyolu Metro Hattı inşaat çalışmaları planlandığı gibi devam ediyor.',
image: 'https://images.pexels.com/photos/17152223/pexels-photo-17152223.jpeg',
date: '20 Ekim 2025',
author: 'Ankara Büyükşehir Belediyesi',
content: 'A2 Metro Hattı kapsamında Dikimevi-Natoyolu güzergâhında yürütülen kazı çalışmaları planlandığı şekilde devam ediyor. 6.5 kilometre uzunluğundaki hat üzerinde 5 istasyon inşa edilecek. Proje, Ankara\'nın ulaşım altyapısına önemli bir katkı sağlayacak. İnşaat ekiplerimiz 7/24 çalışarak projeyi zamanında tamamlamak için büyük çaba gösteriyor.',
tags: ['Metro', 'İnşaat', 'A2 Hattı'],
featured: true
},
{
id: 2,
category: 'announcements',
title: 'Yeni İstasyon Tasarımlarııklandı',
summary: 'Modern ve çevre dostu istasyon tasarımları kamuoyuyla paylaşıldı.',
image: 'https://images.pexels.com/photos/17302615/pexels-photo-17302615.jpeg',
date: '18 Ekim 2025',
author: 'Proje Yönetimi',
content: 'A2 Metro Hattı istasyonlarının mimari tasarımları tamamlandı. Tasarımlarda enerji verimliliği, erişilebilirlik ve modern estetik ön planda tutuldu. İstasyonlarda güneş enerjisi panelleri, LED aydınlatma ve akıllı havalandırma sistemleri kullanılacak. Yolcu konforunu artıracak modern bekleme alanları ve engelsiz erişim imkanları sunulacak.',
tags: ['Tasarım', 'Sürdürülebilirlik', 'İstasyon'],
featured: true
},
{
id: 3,
category: 'construction',
title: 'Dikimevi İstasyonu Temel Atma Töreni Gerçekleştirildi',
summary: 'Dikimevi İstasyonu\'nun temel atma töreni büyük bir coşkuyla yapıldı.',
image: 'https://images.pexels.com/photos/33950678/pexels-photo-33950678.jpeg',
date: '15 Ekim 2025',
author: 'Ankara Büyükşehir Belediyesi',
content: 'A2 Metro Hattı\'nın önemli duraklarından biri olan Dikimevi İstasyonu\'nun temel atma töreni gerçekleştirildi. Törene Ankara Büyükşehir Belediye Başkanı, milletvekilleri ve bölge sakinleri katıldı. İstasyonun 2026 yılı sonunda hizmete girmesi planlanıyor. Dikimevi istasyonu günlük 50 bin yolcu kapasitesine sahip olacak.',
tags: ['Tören', 'Dikimevi', 'Milestone'],
featured: true
},
{
id: 4,
category: 'announcements',
title: 'Metro Hattı 2026\'da Hizmete Girecek',
summary: 'A2 Metro Hattı\'nın ilk etabının 2026 yılı sonunda hizmete girmesi planlanıyor.',
image: 'https://images.pexels.com/photos/253647/pexels-photo-253647.jpeg',
date: '12 Ekim 2025',
author: 'Ankara Büyükşehir Belediyesi',
content: 'Ankara Büyükşehir Belediyesi yetkilileri, A2 Metro Hattı\'nın ilk etabının 2026 yılı sonunda hizmete açılacağını duyurdu. 8 istasyondan oluşan hat, günlük 100 bin yolcuya hizmet verecek. Modern trenler, konforlu istasyonlar ve güvenli sistem ile Ankara\'nın ulaşım altyapısına önemli bir katkı sağlayacak. İlk etabın ardından hatın genişletilmesi planlanıyor.',
tags: ['2026', 'Açılış', 'Milestone'],
featured: true
},
{
id: 5,
category: 'events',
title: 'Metro Projesi Tanıtım Günleri Düzenlendi',
summary: 'Vatandaşlarımız proje hakkında detaylı bilgi aldı.',
image: 'https://images.pexels.com/photos/17152223/pexels-photo-17152223.jpeg',
date: '8 Ekim 2025',
author: 'Halkla İlişkiler',
content: 'A2 Metro Hattı Projesi\'ni tanıtmak amacıyla düzenlenen etkinlikte vatandaşlar, proje yetkilileriyle bir araya geldi. Etkinlikte 3D animasyonlar, maketler ve interaktif sunumlarla proje detayları anlatıldı. Vatandaşların soruları cevaplandı. Etkinliğe büyük ilgi gösterildi.',
tags: ['Tanıtım', 'Etkinlik', 'Halkla İlişkiler']
},
{
id: 6,
category: 'announcements',
title: 'Trafik Düzenlemesi Duyurusu',
summary: 'İnşaat nedeniyle bazı yollarda geçici trafik düzenlemeleri yapılacak.',
image: 'https://images.pexels.com/photos/17302615/pexels-photo-17302615.jpeg',
date: '5 Ekim 2025',
author: 'Ulaşım Koordinasyon',
content: 'A2 Metro Hattı inşaatı kapsamında Dikimevi-Tuzluçayır güzergâhında geçici trafik düzenlemeleri uygulanacak. Alternatif güzergâhlar belirlenmiş olup, yönlendirme levhaları yerleştirilmiştir. Vatandaşlarımızın anlayışına sığınıyoruz. Detaylı bilgi için belediyemizin web sitesini ziyaret edebilirsiniz.',
tags: ['Trafik', 'Duyuru', 'Koordinasyon']
},
{
id: 7,
category: 'construction',
title: 'TBM Makinesi Tünele Girdi',
summary: 'Dev tünel açma makinesi (TBM) çalışmalara başladı.',
image: 'https://images.pexels.com/photos/33950678/pexels-photo-33950678.jpeg',
date: '1 Ekim 2025',
author: 'İnşaat Ekibi',
content: 'A2 Metro Hattı tünel kazı çalışmalarında kullanılacak TBM (Tunnel Boring Machine) makinesi tünele girdi. 120 metrelik dev makine, günde ortalama 15 metre ilerleme kaydedecek. TBM teknolojisi sayesinde kazı çalışmaları daha hızlı ve güvenli şekilde yapılıyor. Makine, en son teknoloji sensörlerle donatılmış durumda.',
tags: ['TBM', 'Tünel', 'Teknoloji']
},
{
id: 8,
category: 'events',
title: 'İş Güvenliği Eğitimi Tamamlandı',
summary: '500+ çalışan iş güvenliği eğitiminden geçti.',
image: 'https://images.pexels.com/photos/253647/pexels-photo-253647.jpeg',
date: '28 Eylül 2025',
author: 'İnsan Kaynakları',
content: 'A2 Metro Hattı projesinde çalışan tüm personel için kapsamlı iş güvenliği eğitimleri düzenlendi. ISO 45001 standartlarına uygun eğitimlerde, inşaat sahası güvenliği, ekipman kullanımı ve acil durum prosedürleri anlatıldı. Sıfır iş kazası hedefiyle çalışıyoruz.',
tags: ['İş Güvenliği', 'Eğitim', 'ISO 45001']
}
];
// Mock data removed - all data comes from database
export const newsData: NewsItem[] = [];
export const categories = [
{ id: 'all', name: 'Tümü', icon: '📰' },

BIN
dev.db Normal file

Binary file not shown.

View File

@@ -18,6 +18,7 @@ const eslintConfig = [
"out/**",
"build/**",
"next-env.d.ts",
"prisma/seed.js",
],
},
];

59
lib/auth.ts Normal file
View File

@@ -0,0 +1,59 @@
import { jwtVerify, SignJWT } from 'jose';
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
const secret = new TextEncoder().encode(
process.env.JWT_SECRET || 'default-secret-key'
);
export interface SessionPayload {
userId: string;
username: string;
iat?: number;
exp?: number;
}
export async function encrypt(payload: SessionPayload) {
return await new SignJWT(payload as unknown as Record<string, unknown>)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('24h')
.sign(secret);
}
export async function decrypt(token: string): Promise<SessionPayload | null> {
try {
const { payload } = await jwtVerify(token, secret, {
algorithms: ['HS256'],
});
return payload as unknown as SessionPayload;
} catch {
return null;
}
}
export async function getSession() {
const cookieStore = await cookies();
const token = cookieStore.get('session')?.value;
if (!token) return null;
return await decrypt(token);
}
export async function withAuth(
request: NextRequest,
handler: (req: NextRequest, session: SessionPayload) => Promise<NextResponse>
) {
const token = request.cookies.get('session')?.value;
if (!token) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const session = await decrypt(token);
if (!session) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
return handler(request, session);
}

View File

@@ -1,4 +1,4 @@
// Veri yönetimi için localStorage tabanlı store
// Veri yönetimi için API tabanlı store
import { newsData, type NewsItem } from '@/data/news';
import { mediaData, type MediaItem } from '@/data/media';
import { documentsData, type Document } from '@/data/documents';
@@ -20,7 +20,7 @@ export interface LiveStreamConfig {
}
export interface Message {
id: string;
id: string | number;
name: string;
email: string;
phone: string;
@@ -69,6 +69,10 @@ export interface SiteSettings {
};
}
// API helper
const API_BASE = '/api';
const isBrowser = typeof window !== 'undefined';
// Default slider data
export const defaultSliderData: SliderItem[] = [
{
@@ -197,147 +201,301 @@ export const defaultCameraData: Camera[] = [
}
];
// LocalStorage keys
const KEYS = {
SLIDER: 'a2metro_slider',
NEWS: 'a2metro_news',
MEDIA: 'a2metro_media',
DOCUMENTS: 'a2metro_documents',
METRO_STATIONS: 'a2metro_stations',
LIVE_STREAM: 'a2metro_live_stream',
SITE_SETTINGS: 'a2metro_site_settings',
MESSAGES: 'a2metro_messages',
FAQS: 'a2metro_faqs',
CAMERAS: 'a2metro_cameras'
};
// Helper functions
const isBrowser = typeof window !== 'undefined';
export const dataStore = {
// Slider
getSlider: (): SliderItem[] => {
getSlider: async (): Promise<SliderItem[]> => {
if (!isBrowser) return defaultSliderData;
const stored = localStorage.getItem(KEYS.SLIDER);
return stored ? JSON.parse(stored) : defaultSliderData;
try {
const res = await fetch(`${API_BASE}/slider`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
return data.length > 0 ? data : defaultSliderData;
} catch (error) {
console.error('Slider fetch error:', error);
return defaultSliderData;
}
},
setSlider: (data: SliderItem[]) => {
if (isBrowser) {
localStorage.setItem(KEYS.SLIDER, JSON.stringify(data));
setSlider: async () => {
// Not used - individual items updated via API
console.warn('setSlider deprecated - use addSlider/updateSlider/deleteSlider');
},
addSlider: async (item: Omit<SliderItem, 'id'>) => {
try {
const res = await fetch(`${API_BASE}/slider`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item),
});
if (!res.ok) throw new Error('Failed to add');
return await res.json();
} catch (error) {
console.error('Slider add error:', error);
throw error;
}
},
updateSlider: async (id: number, item: Partial<SliderItem>) => {
try {
const res = await fetch(`${API_BASE}/slider`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...item }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Slider update error:', error);
throw error;
}
},
deleteSlider: async (id: number) => {
try {
const res = await fetch(`${API_BASE}/slider?id=${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete');
return await res.json();
} catch (error) {
console.error('Slider delete error:', error);
throw error;
}
},
// News
getNews: (): NewsItem[] => {
getNews: async (): Promise<NewsItem[]> => {
if (!isBrowser) return newsData;
const stored = localStorage.getItem(KEYS.NEWS);
return stored ? JSON.parse(stored) : newsData;
},
setNews: (data: NewsItem[]) => {
if (isBrowser) {
localStorage.setItem(KEYS.NEWS, JSON.stringify(data));
try {
const res = await fetch(`${API_BASE}/news`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
return data.length > 0 ? data : newsData;
} catch (error) {
console.error('News fetch error:', error);
return newsData;
}
},
addNews: (newsItem: Omit<NewsItem, 'id'>) => {
const current = dataStore.getNews();
const newItem = { ...newsItem, id: Date.now() };
dataStore.setNews([newItem, ...current]);
return newItem;
setNews: async () => {
console.warn('setNews deprecated - use addNews/updateNews/deleteNews');
},
updateNews: (id: number, newsItem: Partial<NewsItem>) => {
const current = dataStore.getNews();
const updated = current.map((item) =>
item.id === id ? { ...item, ...newsItem } : item
);
dataStore.setNews(updated);
addNews: async (newsItem: Omit<NewsItem, 'id'>) => {
try {
const res = await fetch(`${API_BASE}/news`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newsItem),
});
if (!res.ok) throw new Error('Failed to add');
return await res.json();
} catch (error) {
console.error('News add error:', error);
throw error;
}
},
deleteNews: (id: number) => {
const current = dataStore.getNews();
const filtered = current.filter((item) => item.id !== id);
dataStore.setNews(filtered);
updateNews: async (id: number, newsItem: Partial<NewsItem>) => {
try {
const res = await fetch(`${API_BASE}/news`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...newsItem }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('News update error:', error);
throw error;
}
},
deleteNews: async (id: number) => {
try {
const res = await fetch(`${API_BASE}/news?id=${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete');
return await res.json();
} catch (error) {
console.error('News delete error:', error);
throw error;
}
},
// Media
getMedia: (): MediaItem[] => {
getMedia: async (): Promise<MediaItem[]> => {
if (!isBrowser) return mediaData;
const stored = localStorage.getItem(KEYS.MEDIA);
return stored ? JSON.parse(stored) : mediaData;
},
setMedia: (data: MediaItem[]) => {
if (isBrowser) {
localStorage.setItem(KEYS.MEDIA, JSON.stringify(data));
try {
const res = await fetch(`${API_BASE}/media`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
return data.length > 0 ? data : mediaData;
} catch (error) {
console.error('Media fetch error:', error);
return mediaData;
}
},
addMedia: (mediaItem: Omit<MediaItem, 'id'>) => {
const current = dataStore.getMedia();
const newItem = { ...mediaItem, id: Date.now() };
dataStore.setMedia([newItem, ...current]);
return newItem;
setMedia: async () => {
console.warn('setMedia deprecated - use addMedia/updateMedia/deleteMedia');
},
updateMedia: (id: number, mediaItem: Partial<MediaItem>) => {
const current = dataStore.getMedia();
const updated = current.map((item) =>
item.id === id ? { ...item, ...mediaItem } : item
);
dataStore.setMedia(updated);
addMedia: async (mediaItem: Omit<MediaItem, 'id'>) => {
try {
const res = await fetch(`${API_BASE}/media`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(mediaItem),
});
if (!res.ok) throw new Error('Failed to add');
return await res.json();
} catch (error) {
console.error('Media add error:', error);
throw error;
}
},
deleteMedia: (id: number) => {
const current = dataStore.getMedia();
const filtered = current.filter((item) => item.id !== id);
dataStore.setMedia(filtered);
updateMedia: async (id: number, mediaItem: Partial<MediaItem>) => {
try {
const res = await fetch(`${API_BASE}/media`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...mediaItem }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Media update error:', error);
throw error;
}
},
deleteMedia: async (id: number) => {
try {
const res = await fetch(`${API_BASE}/media?id=${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete');
return await res.json();
} catch (error) {
console.error('Media delete error:', error);
throw error;
}
},
// Documents
getDocuments: (): Document[] => {
getDocuments: async (): Promise<Document[]> => {
if (!isBrowser) return documentsData;
const stored = localStorage.getItem(KEYS.DOCUMENTS);
return stored ? JSON.parse(stored) : documentsData;
},
setDocuments: (data: Document[]) => {
if (isBrowser) {
localStorage.setItem(KEYS.DOCUMENTS, JSON.stringify(data));
try {
const res = await fetch(`${API_BASE}/documents`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
return data.length > 0 ? data : documentsData;
} catch (error) {
console.error('Documents fetch error:', error);
return documentsData;
}
},
addDocument: (document: Omit<Document, 'id'>) => {
const current = dataStore.getDocuments();
const newItem = { ...document, id: Date.now() };
dataStore.setDocuments([newItem, ...current]);
return newItem;
setDocuments: async () => {
console.warn('setDocuments deprecated - use addDocument/updateDocument/deleteDocument');
},
updateDocument: (id: number, document: Partial<Document>) => {
const current = dataStore.getDocuments();
const updated = current.map((item) =>
item.id === id ? { ...item, ...document } : item
);
dataStore.setDocuments(updated);
addDocument: async (document: Omit<Document, 'id'>) => {
try {
const res = await fetch(`${API_BASE}/documents`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(document),
});
if (!res.ok) throw new Error('Failed to add');
return await res.json();
} catch (error) {
console.error('Document add error:', error);
throw error;
}
},
deleteDocument: (id: number) => {
const current = dataStore.getDocuments();
const filtered = current.filter((item) => item.id !== id);
dataStore.setDocuments(filtered);
updateDocument: async (id: number, document: Partial<Document>) => {
try {
const res = await fetch(`${API_BASE}/documents`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...document }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Document update error:', error);
throw error;
}
},
deleteDocument: async (id: number) => {
try {
const res = await fetch(`${API_BASE}/documents?id=${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) {
console.error('Delete document failed - Status:', res.status, res.statusText);
const text = await res.text();
console.error('Response body:', text);
try {
const errorData = JSON.parse(text);
throw new Error(errorData.error || 'Failed to delete');
} catch {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
}
return await res.json();
} catch (error) {
console.error('Document delete error:', error);
throw error;
}
},
// Metro Stations
getMetroStations: (): MetroStation[] => {
getMetroStations: async (): Promise<MetroStation[]> => {
if (!isBrowser) return metroStations;
const stored = localStorage.getItem(KEYS.METRO_STATIONS);
return stored ? JSON.parse(stored) : metroStations;
},
setMetroStations: (data: MetroStation[]) => {
if (isBrowser) {
localStorage.setItem(KEYS.METRO_STATIONS, JSON.stringify(data));
try {
const res = await fetch(`${API_BASE}/stations`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
return data.length > 0 ? data : metroStations;
} catch (error) {
console.error('Stations fetch error:', error);
return metroStations;
}
},
updateStation: (id: number, station: Partial<MetroStation>) => {
const current = dataStore.getMetroStations();
const updated = current.map((item) =>
item.id === id ? { ...item, ...station } : item
);
dataStore.setMetroStations(updated);
setMetroStations: async () => {
console.warn('setMetroStations deprecated - use updateStation');
},
updateStation: async (id: number, station: Partial<MetroStation>) => {
try {
const res = await fetch(`${API_BASE}/stations`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...station }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Station update error:', error);
throw error;
}
},
// Live Stream
getLiveStream: (): LiveStreamConfig => {
getLiveStream: async (): Promise<LiveStreamConfig> => {
if (!isBrowser) {
return {
url: 'https://www.youtube.com/embed/jfKfPfyJRdk?si=example',
@@ -345,62 +503,96 @@ export const dataStore = {
title: 'Canlı Yayın'
};
}
const stored = localStorage.getItem(KEYS.LIVE_STREAM);
return stored ? JSON.parse(stored) : {
url: 'https://www.youtube.com/embed/jfKfPfyJRdk?si=example',
active: true,
title: 'Canlı Yayın'
};
try {
const res = await fetch(`${API_BASE}/live-stream`);
if (!res.ok) throw new Error('Failed to fetch');
return await res.json();
} catch (error) {
console.error('Live stream fetch error:', error);
return {
url: 'https://www.youtube.com/embed/jfKfPfyJRdk?si=example',
active: true,
title: 'Canlı Yayın'
};
}
},
setLiveStream: (config: LiveStreamConfig) => {
if (isBrowser) {
localStorage.setItem(KEYS.LIVE_STREAM, JSON.stringify(config));
setLiveStream: async (config: LiveStreamConfig) => {
try {
const res = await fetch(`${API_BASE}/live-stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(config),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Live stream update error:', error);
throw error;
}
},
// Messages
getMessages: (): Message[] => {
getMessages: async (): Promise<Message[]> => {
if (!isBrowser) return [];
const stored = localStorage.getItem(KEYS.MESSAGES);
return stored ? JSON.parse(stored) : [];
},
addMessage: (message: Omit<Message, 'id' | 'date' | 'read'>) => {
const messages = dataStore.getMessages();
const newMessage: Message = {
...message,
id: Date.now().toString(),
date: new Date().toISOString(),
read: false
};
messages.unshift(newMessage);
if (isBrowser) {
localStorage.setItem(KEYS.MESSAGES, JSON.stringify(messages));
}
return newMessage;
},
markMessageAsRead: (id: string) => {
const messages = dataStore.getMessages();
const updated = messages.map(msg =>
msg.id === id ? { ...msg, read: true } : msg
);
if (isBrowser) {
localStorage.setItem(KEYS.MESSAGES, JSON.stringify(updated));
try {
const res = await fetch(`${API_BASE}/messages`);
if (!res.ok) throw new Error('Failed to fetch');
return await res.json();
} catch (error) {
console.error('Messages fetch error:', error);
return [];
}
},
deleteMessage: (id: string) => {
const messages = dataStore.getMessages();
const filtered = messages.filter(msg => msg.id !== id);
if (isBrowser) {
localStorage.setItem(KEYS.MESSAGES, JSON.stringify(filtered));
addMessage: async (message: Omit<Message, 'id' | 'createdAt' | 'updatedAt'>) => {
try {
const res = await fetch(`${API_BASE}/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(message),
});
if (!res.ok) throw new Error('Failed to add');
return await res.json();
} catch (error) {
console.error('Message add error:', error);
throw error;
}
},
markMessageAsRead: async (id: string | number) => {
try {
const res = await fetch(`${API_BASE}/messages`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: typeof id === 'string' ? parseInt(id) : id, read: true }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Message update error:', error);
throw error;
}
},
deleteMessage: async (id: string | number) => {
try {
const res = await fetch(`${API_BASE}/messages?id=${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete');
return await res.json();
} catch (error) {
console.error('Message delete error:', error);
throw error;
}
},
// Site Settings
getSiteSettings: (): SiteSettings => {
getSiteSettings: async (): Promise<SiteSettings> => {
if (!isBrowser) {
return {
contact: {
@@ -423,133 +615,185 @@ export const dataStore = {
}
};
}
const stored = localStorage.getItem(KEYS.SITE_SETTINGS);
if (stored) return JSON.parse(stored);
// Default settings
const defaultSettings: SiteSettings = {
contact: {
phone: '+90 (312) 123 45 67',
email: 'info@a2metro.com.tr',
address: 'Ankara Teknokent, Cyberpark C Blok Kat:3 No:301 Çankaya/ANKARA',
kep: 'a2metro@hs03.kep.tr'
},
social: {
facebook: 'https://facebook.com/a2metro',
twitter: 'https://twitter.com/a2metro',
instagram: 'https://instagram.com/a2metro',
youtube: 'https://youtube.com/@a2metro',
linkedin: 'https://linkedin.com/company/a2metro'
},
companyInfo: {
name: 'A2 Metro',
fullName: 'A2 Metro Yapı ve İnşaat A.Ş.',
foundedYear: '2010'
}
};
localStorage.setItem(KEYS.SITE_SETTINGS, JSON.stringify(defaultSettings));
return defaultSettings;
try {
const res = await fetch(`${API_BASE}/settings`);
if (!res.ok) throw new Error('Failed to fetch');
return await res.json();
} catch (error) {
console.error('Settings fetch error:', error);
return {
contact: {
phone: '+90 (312) 123 45 67',
email: 'info@a2metro.com.tr',
address: 'Ankara Teknokent, Cyberpark C Blok Kat:3 No:301 Çankaya/ANKARA',
kep: 'a2metro@hs03.kep.tr'
},
social: {
facebook: 'https://facebook.com/a2metro',
twitter: 'https://twitter.com/a2metro',
instagram: 'https://instagram.com/a2metro',
youtube: 'https://youtube.com/@a2metro',
linkedin: 'https://linkedin.com/company/a2metro'
},
companyInfo: {
name: 'A2 Metro',
fullName: 'A2 Metro Yapı ve İnşaat A.Ş.',
foundedYear: '2010'
}
};
}
},
updateSiteSettings: (settings: Partial<SiteSettings>) => {
const current = dataStore.getSiteSettings();
const updated = {
...current,
...settings,
contact: { ...current.contact, ...(settings.contact || {}) },
social: { ...current.social, ...(settings.social || {}) },
companyInfo: { ...current.companyInfo, ...(settings.companyInfo || {}) }
};
if (isBrowser) {
localStorage.setItem(KEYS.SITE_SETTINGS, JSON.stringify(updated));
updateSiteSettings: async (settings: Partial<SiteSettings>) => {
try {
const current = await dataStore.getSiteSettings();
const updated = {
...current,
...settings,
contact: { ...current.contact, ...(settings.contact || {}) },
social: { ...current.social, ...(settings.social || {}) },
companyInfo: { ...current.companyInfo, ...(settings.companyInfo || {}) }
};
const res = await fetch(`${API_BASE}/settings`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updated),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Settings update error:', error);
throw error;
}
},
// FAQs
getFAQs: (): FAQ[] => {
getFAQs: async (): Promise<FAQ[]> => {
if (!isBrowser) return defaultFAQData;
const stored = localStorage.getItem(KEYS.FAQS);
return stored ? JSON.parse(stored) : defaultFAQData;
},
setFAQs: (faqs: FAQ[]) => {
if (isBrowser) {
localStorage.setItem(KEYS.FAQS, JSON.stringify(faqs));
try {
const res = await fetch(`${API_BASE}/faqs`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
return data.length > 0 ? data : defaultFAQData;
} catch (error) {
console.error('FAQs fetch error:', error);
return defaultFAQData;
}
},
addFAQ: (faq: Omit<FAQ, 'id'>) => {
const faqs = dataStore.getFAQs();
const newFAQ: FAQ = {
...faq,
id: faqs.length > 0 ? Math.max(...faqs.map(f => f.id)) + 1 : 1
};
dataStore.setFAQs([...faqs, newFAQ]);
return newFAQ;
setFAQs: async () => {
console.warn('setFAQs deprecated - use addFAQ/updateFAQ/deleteFAQ');
},
updateFAQ: (id: number, updates: Partial<FAQ>) => {
const faqs = dataStore.getFAQs();
const updated = faqs.map(faq =>
faq.id === id ? { ...faq, ...updates } : faq
);
dataStore.setFAQs(updated);
addFAQ: async (faq: Omit<FAQ, 'id'>) => {
try {
const res = await fetch(`${API_BASE}/faqs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(faq),
});
if (!res.ok) throw new Error('Failed to add');
return await res.json();
} catch (error) {
console.error('FAQ add error:', error);
throw error;
}
},
deleteFAQ: (id: number) => {
const faqs = dataStore.getFAQs();
dataStore.setFAQs(faqs.filter(faq => faq.id !== id));
updateFAQ: async (id: number, updates: Partial<FAQ>) => {
try {
const res = await fetch(`${API_BASE}/faqs`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...updates }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('FAQ update error:', error);
throw error;
}
},
deleteFAQ: async (id: number) => {
try {
const res = await fetch(`${API_BASE}/faqs?id=${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete');
return await res.json();
} catch (error) {
console.error('FAQ delete error:', error);
throw error;
}
},
// Cameras
getCameras: (): Camera[] => {
getCameras: async (): Promise<Camera[]> => {
if (!isBrowser) return defaultCameraData;
const stored = localStorage.getItem(KEYS.CAMERAS);
return stored ? JSON.parse(stored) : defaultCameraData;
},
setCameras: (cameras: Camera[]) => {
if (isBrowser) {
localStorage.setItem(KEYS.CAMERAS, JSON.stringify(cameras));
try {
const res = await fetch(`${API_BASE}/cameras`);
if (!res.ok) throw new Error('Failed to fetch');
const data = await res.json();
return data.length > 0 ? data : defaultCameraData;
} catch (error) {
console.error('Cameras fetch error:', error);
return defaultCameraData;
}
},
addCamera: (camera: Omit<Camera, 'id'>) => {
const cameras = dataStore.getCameras();
const newCamera: Camera = {
...camera,
id: cameras.length > 0 ? Math.max(...cameras.map(c => c.id)) + 1 : 1
};
dataStore.setCameras([...cameras, newCamera]);
return newCamera;
setCameras: async () => {
console.warn('setCameras deprecated - use addCamera/updateCamera/deleteCamera');
},
updateCamera: (id: number, updates: Partial<Camera>) => {
const cameras = dataStore.getCameras();
const updated = cameras.map(camera =>
camera.id === id ? { ...camera, ...updates } : camera
);
dataStore.setCameras(updated);
addCamera: async (camera: Omit<Camera, 'id' | 'createdAt' | 'updatedAt'>) => {
try {
const res = await fetch(`${API_BASE}/cameras`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify(camera),
});
if (!res.ok) throw new Error('Failed to add');
return await res.json();
} catch (error) {
console.error('Camera add error:', error);
throw error;
}
},
deleteCamera: (id: number) => {
const cameras = dataStore.getCameras();
dataStore.setCameras(cameras.filter(camera => camera.id !== id));
updateCamera: async (id: number, updates: Partial<Camera>) => {
try {
const res = await fetch(`${API_BASE}/cameras`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, ...updates }),
});
if (!res.ok) throw new Error('Failed to update');
return await res.json();
} catch (error) {
console.error('Camera update error:', error);
throw error;
}
},
// Reset all data
deleteCamera: async (id: number) => {
try {
const res = await fetch(`${API_BASE}/cameras?id=${id}`, {
method: 'DELETE',
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to delete');
return await res.json();
} catch (error) {
console.error('Camera delete error:', error);
throw error;
}
},
// Reset all data (Not applicable for API-based storage)
resetAll: () => {
if (isBrowser) {
localStorage.removeItem(KEYS.SLIDER);
localStorage.removeItem(KEYS.NEWS);
localStorage.removeItem(KEYS.MEDIA);
localStorage.removeItem(KEYS.DOCUMENTS);
localStorage.removeItem(KEYS.METRO_STATIONS);
localStorage.removeItem(KEYS.LIVE_STREAM);
localStorage.removeItem(KEYS.SITE_SETTINGS);
localStorage.removeItem(KEYS.MESSAGES);
localStorage.removeItem(KEYS.FAQS);
localStorage.removeItem(KEYS.CAMERAS);
}
console.warn('resetAll not applicable for API-based storage');
}
};

9
lib/prisma.ts Normal file
View File

@@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

931
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,22 +6,34 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"db:push": "prisma db push",
"db:seed": "tsx prisma/seed.mts",
"db:reset": "prisma db push --force-reset && npm run db:seed"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"bcryptjs": "^3.0.3",
"jose": "^6.1.2",
"jsonwebtoken": "^9.0.2",
"next": "15.5.6",
"prisma": "^5.22.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"next": "15.5.6"
"react-dom": "19.1.0"
},
"devDependencies": {
"typescript": "^5",
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9",
"eslint-config-next": "15.5.6",
"@eslint/eslintrc": "^3"
"tailwindcss": "^4",
"ts-node": "^10.9.2",
"tsx": "^4.20.6",
"typescript": "^5"
}
}

BIN
prisma/dev.db Normal file

Binary file not shown.

138
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,138 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
role String @default("admin")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SliderItem {
id Int @id @default(autoincrement())
title String
description String
buttonText String
buttonLink String
active Boolean @default(true)
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model News {
id Int @id @default(autoincrement())
title String
summary String
content String
image String
category String
author String
tags String
featured Boolean @default(false)
date String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Media {
id Int @id @default(autoincrement())
type String
title String
description String
thumbnail String
videoUrl String?
duration String?
category String?
featured Boolean @default(false)
date String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Document {
id Int @id @default(autoincrement())
title String
description String
type String
category String
size String
downloadUrl String
date String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model MetroStation {
id Int @id @default(autoincrement())
name String
status String
progress Int @default(0)
description String?
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model LiveStream {
id Int @id @default(autoincrement())
url String
title String?
active Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Message {
id Int @id @default(autoincrement())
name String
email String
phone String
subject String
type String
message String
read Boolean @default(false)
date String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model FAQ {
id Int @id @default(autoincrement())
question String
answer String
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Camera {
id Int @id @default(autoincrement())
name String
location String
videoUrl String
status String @default("online")
viewers Int @default(0)
order Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SiteSettings {
id Int @id @default(autoincrement())
key String @unique
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

173
prisma/seed.js Normal file
View File

@@ -0,0 +1,173 @@
const { PrismaClient } = require('@prisma/client');
const { hash } = require('bcryptjs');
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// Admin kullanıcı oluştur
const adminPassword = await hash(
process.env.ADMIN_DEFAULT_PASSWORD || 'admin123',
10
);
const admin = await prisma.user.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
password: adminPassword,
},
});
console.log('✓ Admin kullanıcı oluşturuldu:', admin.username);
// Slider items
const sliderItems = [
{
title: 'Ankara Metro Altyapı Projelerinde Öncü Çözümler',
description:
'Ankara Büyükşehir Belediyesi ile birlikte, modern teknoloji ve mühendislik uzmanlığımızla başkentin ulaşım ağını inşa ediyor, geleceğin metro sistemlerini bugünden hayata geçiriyoruz.',
buttonText: 'Detayları Gör',
buttonLink: '#proje-detay',
active: true,
},
{
title: 'A2 Metro Hattı İnşaatında Son Aşamaya Gelindi',
description:
'15 istasyonlu A2 Metro Hattı projemiz %75 tamamlandı. 2026 yılında hizmete açılacak modern metro hattımız, günlük 300 bin yolcuya hizmet verecek.',
buttonText: 'İlerlemeyi İzle',
buttonLink: '#metro-hatti',
active: true,
},
{
title: 'Çevre Dostu Metro Teknolojileri',
description:
'Yenilenebilir enerji kaynakları ve sürdürülebilir inşaat teknikleri ile çevre dostu metro projelerine imza atıyoruz. Karbon emisyonunu %40 azaltan yenilikçi çözümlerimiz.',
buttonText: 'Yeşil Projeler',
buttonLink: '#cevre',
active: true,
},
];
for (const item of sliderItems) {
await prisma.sliderItem.create({ data: item });
}
console.log('✓ Slider items oluşturuldu:', sliderItems.length);
// FAQs
const faqs = [
{
question: 'Metro inşaat çalışmaları ne zaman başladı?',
answer:
'A2 Metro Hattı inşaat çalışmalarımız 2021 yılında başlamış olup, proje 2026 yılında tamamlanacaktır.',
order: 1,
},
{
question: 'Hangi metro hatlarında çalışıyorsunuz?',
answer:
'Şu anda A2 Metro Hattı, Keçiören-Gölbaşı Metro Hattı ve Batıkent-Sincan Metro Hattı projelerinde aktif olarak çalışmaktayız.',
order: 2,
},
{
question: 'Çevreye olan etkiniz nedir?',
answer:
'Tüm projelerimizde çevre dostu teknolojiler kullanıyor, karbon emisyonunu en aza indirmeye özen gösteriyoruz. Ayrıca geri dönüşüm programlarımızla inşaat atıklarını azaltıyoruz.',
order: 3,
},
];
for (const faq of faqs) {
await prisma.fAQ.create({ data: faq });
}
console.log('✓ FAQs oluşturuldu:', faqs.length);
// Cameras
const cameras = [
{
name: 'A2 Metro - Ana Şantiye',
location: 'Kızılay İstasyonu',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'online',
viewers: 1245,
order: 1,
},
{
name: 'Keçiören Hattı - Tünel Kazı',
location: 'Keçiören İstasyonu',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'online',
viewers: 892,
order: 2,
},
{
name: 'Gölbaşı İstasyonu İnşaatı',
location: 'Gölbaşı',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'offline',
viewers: 0,
order: 3,
},
];
for (const camera of cameras) {
await prisma.camera.create({ data: camera });
}
console.log('✓ Cameras oluşturuldu:', cameras.length);
// Site Settings
await prisma.siteSettings.create({
data: {
key: 'main',
value: JSON.stringify({
contact: {
phone: '+90 (312) 555 00 00',
email: 'info@gulermakmetro.com.tr',
address:
'Ankara Büyükşehir Belediyesi İş Merkezi, Çankaya/Ankara',
kep: 'gulermak@hs01.kep.tr',
},
social: {
facebook: 'https://facebook.com/gulermakmetro',
twitter: 'https://twitter.com/gulermakmetro',
instagram: 'https://instagram.com/gulermakmetro',
youtube: 'https://youtube.com/@gulermakmetro',
linkedin: 'https://linkedin.com/company/gulermakmetro',
},
companyInfo: {
name: 'Gülermak Metro',
fullName: 'Gülermak Ankara Metro Yapım A.Ş.',
foundedYear: '2020',
},
}),
},
});
console.log('✓ Site settings oluşturuldu');
// Live Stream
await prisma.liveStream.create({
data: {
url: 'https://www.youtube.com/embed/jfKfPfyJRdk',
active: true,
title: 'A2 Metro Hattı Canlı Yayını',
},
});
console.log('✓ Live stream oluşturuldu');
console.log('✅ Seeding tamamlandı!');
}
main()
.catch((e) => {
console.error('Seeding hatası:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

173
prisma/seed.mts Normal file
View File

@@ -0,0 +1,173 @@
import { PrismaClient } from '@prisma/client';
import { hash } from 'bcryptjs';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// Admin kullanıcı oluştur
const adminPassword = await hash(
process.env.ADMIN_DEFAULT_PASSWORD || 'admin123',
10
);
const admin = await prisma.user.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
password: adminPassword,
},
});
console.log('✓ Admin kullanıcı oluşturuldu:', admin.username);
// Slider items
const sliderItems = [
{
title: 'Ankara Metro Altyapı Projelerinde Öncü Çözümler',
description:
'Ankara Büyükşehir Belediyesi ile birlikte, modern teknoloji ve mühendislik uzmanlığımızla başkentin ulaşım ağını inşa ediyor, geleceğin metro sistemlerini bugünden hayata geçiriyoruz.',
buttonText: 'Detayları Gör',
buttonLink: '#proje-detay',
active: true,
},
{
title: 'A2 Metro Hattı İnşaatında Son Aşamaya Gelindi',
description:
'15 istasyonlu A2 Metro Hattı projemiz %75 tamamlandı. 2026 yılında hizmete açılacak modern metro hattımız, günlük 300 bin yolcuya hizmet verecek.',
buttonText: 'İlerlemeyi İzle',
buttonLink: '#metro-hatti',
active: true,
},
{
title: 'Çevre Dostu Metro Teknolojileri',
description:
'Yenilenebilir enerji kaynakları ve sürdürülebilir inşaat teknikleri ile çevre dostu metro projelerine imza atıyoruz. Karbon emisyonunu %40 azaltan yenilikçi çözümlerimiz.',
buttonText: 'Yeşil Projeler',
buttonLink: '#cevre',
active: true,
},
];
for (const item of sliderItems) {
await prisma.sliderItem.create({ data: item });
}
console.log('✓ Slider items oluşturuldu:', sliderItems.length);
// FAQs
const faqs = [
{
question: 'Metro inşaat çalışmaları ne zaman başladı?',
answer:
'A2 Metro Hattı inşaat çalışmalarımız 2021 yılında başlamış olup, proje 2026 yılında tamamlanacaktır.',
order: 1,
},
{
question: 'Hangi metro hatlarında çalışıyorsunuz?',
answer:
'Şu anda A2 Metro Hattı, Keçiören-Gölbaşı Metro Hattı ve Batıkent-Sincan Metro Hattı projelerinde aktif olarak çalışmaktayız.',
order: 2,
},
{
question: 'Çevreye olan etkiniz nedir?',
answer:
'Tüm projelerimizde çevre dostu teknolojiler kullanıyor, karbon emisyonunu en aza indirmeye özen gösteriyoruz. Ayrıca geri dönüşüm programlarımızla inşaat atıklarını azaltıyoruz.',
order: 3,
},
];
for (const faq of faqs) {
await prisma.fAQ.create({ data: faq });
}
console.log('✓ FAQs oluşturuldu:', faqs.length);
// Cameras
const cameras = [
{
name: 'A2 Metro - Ana Şantiye',
location: 'Kızılay İstasyonu',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'online' as const,
viewers: 1245,
order: 1,
},
{
name: 'Keçiören Hattı - Tünel Kazı',
location: 'Keçiören İstasyonu',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'online' as const,
viewers: 892,
order: 2,
},
{
name: 'Gölbaşı İstasyonu İnşaatı',
location: 'Gölbaşı',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'offline' as const,
viewers: 0,
order: 3,
},
];
for (const camera of cameras) {
await prisma.camera.create({ data: camera });
}
console.log('✓ Cameras oluşturuldu:', cameras.length);
// Site Settings
await prisma.siteSettings.create({
data: {
key: 'main',
value: JSON.stringify({
contact: {
phone: '+90 (312) 555 00 00',
email: 'info@gulermakmetro.com.tr',
address:
'Ankara Büyükşehir Belediyesi İş Merkezi, Çankaya/Ankara',
kep: 'gulermak@hs01.kep.tr',
},
social: {
facebook: 'https://facebook.com/gulermakmetro',
twitter: 'https://twitter.com/gulermakmetro',
instagram: 'https://instagram.com/gulermakmetro',
youtube: 'https://youtube.com/@gulermakmetro',
linkedin: 'https://linkedin.com/company/gulermakmetro',
},
companyInfo: {
name: 'Gülermak Metro',
fullName: 'Gülermak Ankara Metro Yapım A.Ş.',
foundedYear: '2020',
},
}),
},
});
console.log('✓ Site settings oluşturuldu');
// Live Stream
await prisma.liveStream.create({
data: {
url: 'https://www.youtube.com/embed/jfKfPfyJRdk',
active: true,
title: 'A2 Metro Hattı Canlı Yayını',
},
});
console.log('✓ Live stream oluşturuldu');
console.log('✅ Seeding tamamlandı!');
}
main()
.catch((e) => {
console.error('Seeding hatası:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

173
prisma/seed.ts Normal file
View File

@@ -0,0 +1,173 @@
import { hash } from 'bcryptjs';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// Admin kullanıcı oluştur
const adminPassword = await hash(
process.env.ADMIN_DEFAULT_PASSWORD || 'admin123',
10
);
const admin = await prisma.user.upsert({
where: { username: 'admin' },
update: {},
create: {
username: 'admin',
password: adminPassword,
},
});
console.log('✓ Admin kullanıcı oluşturuldu:', admin.username);
// Slider items
const sliderItems = [
{
title: 'Ankara Metro Altyapı Projelerinde Öncü Çözümler',
description:
'Ankara Büyükşehir Belediyesi ile birlikte, modern teknoloji ve mühendislik uzmanlığımızla başkentin ulaşım ağını inşa ediyor, geleceğin metro sistemlerini bugünden hayata geçiriyoruz.',
buttonText: 'Detayları Gör',
buttonLink: '#proje-detay',
active: true,
},
{
title: 'A2 Metro Hattı İnşaatında Son Aşamaya Gelindi',
description:
'15 istasyonlu A2 Metro Hattı projemiz %75 tamamlandı. 2026 yılında hizmete açılacak modern metro hattımız, günlük 300 bin yolcuya hizmet verecek.',
buttonText: 'İlerlemeyi İzle',
buttonLink: '#metro-hatti',
active: true,
},
{
title: 'Çevre Dostu Metro Teknolojileri',
description:
'Yenilenebilir enerji kaynakları ve sürdürülebilir inşaat teknikleri ile çevre dostu metro projelerine imza atıyoruz. Karbon emisyonunu %40 azaltan yenilikçi çözümlerimiz.',
buttonText: 'Yeşil Projeler',
buttonLink: '#cevre',
active: true,
},
];
for (const item of sliderItems) {
await prisma.sliderItem.create({ data: item });
}
console.log('✓ Slider items oluşturuldu:', sliderItems.length);
// FAQs
const faqs = [
{
question: 'Metro inşaat çalışmaları ne zaman başladı?',
answer:
'A2 Metro Hattı inşaat çalışmalarımız 2021 yılında başlamış olup, proje 2026 yılında tamamlanacaktır.',
order: 1,
},
{
question: 'Hangi metro hatlarında çalışıyorsunuz?',
answer:
'Şu anda A2 Metro Hattı, Keçiören-Gölbaşı Metro Hattı ve Batıkent-Sincan Metro Hattı projelerinde aktif olarak çalışmaktayız.',
order: 2,
},
{
question: 'Çevreye olan etkiniz nedir?',
answer:
'Tüm projelerimizde çevre dostu teknolojiler kullanıyor, karbon emisyonunu en aza indirmeye özen gösteriyoruz. Ayrıca geri dönüşüm programlarımızla inşaat atıklarını azaltıyoruz.',
order: 3,
},
];
for (const faq of faqs) {
await prisma.fAQ.create({ data: faq });
}
console.log('✓ FAQs oluşturuldu:', faqs.length);
// Cameras
const cameras = [
{
name: 'A2 Metro - Ana Şantiye',
location: 'Kızılay İstasyonu',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'online',
viewers: 1245,
order: 1,
},
{
name: 'Keçiören Hattı - Tünel Kazı',
location: 'Keçiören İstasyonu',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'online',
viewers: 892,
order: 2,
},
{
name: 'Gölbaşı İstasyonu İnşaatı',
location: 'Gölbaşı',
videoUrl: 'https://www.youtube.com/embed/dQw4w9WgXcQ',
status: 'offline',
viewers: 0,
order: 3,
},
];
for (const camera of cameras) {
await prisma.camera.create({ data: camera });
}
console.log('✓ Cameras oluşturuldu:', cameras.length);
// Site Settings
await prisma.siteSettings.create({
data: {
key: 'main',
value: JSON.stringify({
contact: {
phone: '+90 (312) 555 00 00',
email: 'info@gulermakmetro.com.tr',
address:
'Ankara Büyükşehir Belediyesi İş Merkezi, Çankaya/Ankara',
kep: 'gulermak@hs01.kep.tr',
},
social: {
facebook: 'https://facebook.com/gulermakmetro',
twitter: 'https://twitter.com/gulermakmetro',
instagram: 'https://instagram.com/gulermakmetro',
youtube: 'https://youtube.com/@gulermakmetro',
linkedin: 'https://linkedin.com/company/gulermakmetro',
},
companyInfo: {
name: 'Gülermak Metro',
fullName: 'Gülermak Ankara Metro Yapım A.Ş.',
foundedYear: '2020',
},
}),
},
});
console.log('✓ Site settings oluşturuldu');
// Live Stream
await prisma.liveStream.create({
data: {
url: 'https://www.youtube.com/embed/jfKfPfyJRdk',
active: true,
title: 'A2 Metro Hattı Canlı Yayını',
},
});
console.log('✓ Live stream oluşturuldu');
console.log('✅ Seeding tamamlandı!');
}
main()
.catch((e) => {
console.error('Seeding hatası:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});