diff --git a/.gitignore b/.gitignore index 5ef6a52..62fcee8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +/lib/generated/prisma diff --git a/ADMIN_CREDENTIALS.md b/ADMIN_CREDENTIALS.md new file mode 100644 index 0000000..2af9bdd --- /dev/null +++ b/ADMIN_CREDENTIALS.md @@ -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;" +``` diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..ec69753 --- /dev/null +++ b/app/api/auth/login/route.ts @@ -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 } + ); + } +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..4c5f277 --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -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; +} diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts new file mode 100644 index 0000000..116e204 --- /dev/null +++ b/app/api/auth/session/route.ts @@ -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, + }, + }); +} diff --git a/app/api/cameras/route.ts b/app/api/cameras/route.ts new file mode 100644 index 0000000..edde3e0 --- /dev/null +++ b/app/api/cameras/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/documents/route.ts b/app/api/documents/route.ts new file mode 100644 index 0000000..295a780 --- /dev/null +++ b/app/api/documents/route.ts @@ -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 } + ); + } + }); +} + diff --git a/app/api/faqs/route.ts b/app/api/faqs/route.ts new file mode 100644 index 0000000..cf348da --- /dev/null +++ b/app/api/faqs/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/live-stream/route.ts b/app/api/live-stream/route.ts new file mode 100644 index 0000000..16f306a --- /dev/null +++ b/app/api/live-stream/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/media/route.ts b/app/api/media/route.ts new file mode 100644 index 0000000..3f0d0cd --- /dev/null +++ b/app/api/media/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts new file mode 100644 index 0000000..57f6074 --- /dev/null +++ b/app/api/messages/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/news/route.ts b/app/api/news/route.ts new file mode 100644 index 0000000..423bab8 --- /dev/null +++ b/app/api/news/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts new file mode 100644 index 0000000..11e6313 --- /dev/null +++ b/app/api/settings/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/slider/route.ts b/app/api/slider/route.ts new file mode 100644 index 0000000..aff0501 --- /dev/null +++ b/app/api/slider/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/api/stations/route.ts b/app/api/stations/route.ts new file mode 100644 index 0000000..b017a67 --- /dev/null +++ b/app/api/stations/route.ts @@ -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 } + ); + } + }); +} diff --git a/app/belgeler/page.tsx b/app/belgeler/page.tsx index 559836c..77b47af 100644 --- a/app/belgeler/page.tsx +++ b/app/belgeler/page.tsx @@ -11,7 +11,7 @@ export default function Documents() { const [selectedCategory, setSelectedCategory] = useState('all'); useEffect(() => { - setDocuments(dataStore.getDocuments()); + dataStore.getDocuments().then(setDocuments); }, []); const categories = [ diff --git a/app/canli-yayin/page.tsx b/app/canli-yayin/page.tsx index d256150..976b2bb 100644 --- a/app/canli-yayin/page.tsx +++ b/app/canli-yayin/page.tsx @@ -11,13 +11,15 @@ export default function LiveStream() { const [cameras, setCameras] = useState([]); 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]; diff --git a/app/haberler/page.tsx b/app/haberler/page.tsx index 0e32deb..b65569f 100644 --- a/app/haberler/page.tsx +++ b/app/haberler/page.tsx @@ -13,7 +13,7 @@ export default function News() { const [selectedNews, setSelectedNews] = useState(null); useEffect(() => { - setNewsData(dataStore.getNews()); + dataStore.getNews().then(setNewsData); }, []); const filteredNews = selectedCategory === 'all' diff --git a/app/iletisim/page.tsx b/app/iletisim/page.tsx index 97458be..d74c367 100644 --- a/app/iletisim/page.tsx +++ b/app/iletisim/page.tsx @@ -7,11 +7,51 @@ import { dataStore, type SiteSettings } from '@/lib/dataStore'; export default function Contact() { const [settings, setSettings] = useState(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() {

-
{ - e.preventDefault(); - alert('Mesajınız gönderildi! (Demo)'); - }}> + {/* İki Sütunlu Alan */}
{/* 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() { 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..." > @@ -246,12 +293,25 @@ export default function Contact() {
diff --git a/app/medya/page.tsx b/app/medya/page.tsx index 4cac2bf..3ec314c 100644 --- a/app/medya/page.tsx +++ b/app/medya/page.tsx @@ -13,7 +13,7 @@ export default function MediaGallery() { const [selectedMedia, setSelectedMedia] = useState(null); useEffect(() => { - setMediaItems(dataStore.getMedia()); + dataStore.getMedia().then(setMediaItems); }, []); const filteredMedia = selectedTab === 'all' diff --git a/app/page.tsx b/app/page.tsx index 68217de..ff1737e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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([]); + const [newsData, setNewsData] = useState([]); + const [liveStreamConfig, setLiveStreamConfig] = useState({ 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) diff --git a/app/sss/page.tsx b/app/sss/page.tsx index b1868e7..1504196 100644 --- a/app/sss/page.tsx +++ b/app/sss/page.tsx @@ -11,7 +11,9 @@ export default function FAQPage() { const [faqs, setFaqs] = useState([]); 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) => { diff --git a/app/yonetim-paneli-a2m-secure/dashboard/page.tsx b/app/yonetim-paneli-a2m-secure/dashboard/page.tsx index dd52b85..bbea5b6 100644 --- a/app/yonetim-paneli-a2m-secure/dashboard/page.tsx +++ b/app/yonetim-paneli-a2m-secure/dashboard/page.tsx @@ -36,22 +36,45 @@ export default function Dashboard() { loadData(); }, [router]); - const loadData = () => { - setSliderItems(dataStore.getSlider()); - setNewsItems(dataStore.getNews()); - setMediaItems(dataStore.getMedia()); - setDocuments(dataStore.getDocuments()); - setMetroStations(dataStore.getMetroStations()); - setLiveStreamConfig(dataStore.getLiveStream()); - setMessages(dataStore.getMessages()); - setSiteSettings(dataStore.getSiteSettings()); - setFaqs(dataStore.getFAQs().sort((a, b) => a.order - b.order)); - setCameras(dataStore.getCameras().sort((a, b) => a.order - b.order)); + const loadData = async () => { + try { + const [slider, news, media, docs, stations, liveStream, msgs, settings, faqList, cameraList] = await Promise.all([ + dataStore.getSlider(), + dataStore.getNews(), + dataStore.getMedia(), + dataStore.getDocuments(), + dataStore.getMetroStations(), + dataStore.getLiveStream(), + dataStore.getMessages(), + dataStore.getSiteSettings(), + dataStore.getFAQs(), + dataStore.getCameras(), + ]); + + setSliderItems(slider); + setNewsItems(news); + setMediaItems(media); + setDocuments(docs); + setMetroStations(stations); + setLiveStreamConfig(liveStream); + setMessages(msgs); + setSiteSettings(settings); + setFaqs(faqList.sort((a, b) => a.order - b.order)); + setCameras(cameraList.sort((a, b) => a.order - b.order)); + } catch (error) { + console.error('Veri yükleme hatası:', error); + } }; - const handleLogout = () => { - localStorage.removeItem('admin_token'); - router.push('/yonetim-paneli-a2m-secure'); + const handleLogout = async () => { + try { + await fetch('/api/auth/logout', { method: 'POST' }); + localStorage.removeItem('admin_token'); + router.push('/yonetim-paneli-a2m-secure'); + } catch (error) { + console.error('Logout error:', error); + router.push('/yonetim-paneli-a2m-secure'); + } }; const stats = [ @@ -76,26 +99,23 @@ export default function Dashboard() { ]; // Slider management functions - const toggleSlideActive = (id: number) => { - const updated = sliderItems.map(item => - item.id === id ? { ...item, active: !item.active } : item - ); - setSliderItems(updated); - dataStore.setSlider(updated); + const toggleSlideActive = async (id: number) => { + const item = sliderItems.find(s => s.id === id); + if (item) { + await dataStore.updateSlider(id, { active: !item.active }); + loadData(); + } }; - const updateSlide = (id: number, updates: Partial) => { - const updated = sliderItems.map(item => - item.id === id ? { ...item, ...updates } : item - ); - setSliderItems(updated); - dataStore.setSlider(updated); + const updateSlide = async (id: number, updates: Partial) => { + await dataStore.updateSlider(id, updates); + loadData(); }; // News management functions - const handleDeleteNews = (id: number) => { + const handleDeleteNews = async (id: number) => { if (confirm('Bu haberi silmek istediğinizden emin misiniz?')) { - dataStore.deleteNews(id); + await dataStore.deleteNews(id); loadData(); } }; @@ -109,17 +129,17 @@ export default function Dashboard() { }; // Media management functions - const handleDeleteMedia = (id: number) => { + const handleDeleteMedia = async (id: number) => { if (confirm('Bu medya içeriğini silmek istediğinizden emin misiniz?')) { - dataStore.deleteMedia(id); + await dataStore.deleteMedia(id); loadData(); } }; // Document management functions - const handleDeleteDocument = (id: number) => { + const handleDeleteDocument = async (id: number) => { if (confirm('Bu belgeyi silmek istediğinizden emin misiniz?')) { - dataStore.deleteDocument(id); + await dataStore.deleteDocument(id); loadData(); } }; @@ -204,19 +224,16 @@ export default function Dashboard() { setShowSlideModal(true); }; - const handleSaveSlide = (e: React.FormEvent) => { + const handleSaveSlide = async (e: React.FormEvent) => { e.preventDefault(); if (editingSlide) { const isNew = !sliderItems.find(s => s.id === editingSlide.id); if (isNew) { - // Yeni slider ekle - const updated = [...sliderItems, editingSlide]; - setSliderItems(updated); - dataStore.setSlider(updated); + await dataStore.addSlider(editingSlide); } else { - // Mevcut slider'ı güncelle - updateSlide(editingSlide.id, editingSlide); + await dataStore.updateSlider(editingSlide.id, editingSlide); } + loadData(); setShowSlideModal(false); setEditingSlide(null); } @@ -266,14 +283,14 @@ export default function Dashboard() { setShowNewsModal(true); }; - const handleSaveNews = (e: React.FormEvent) => { + const handleSaveNews = async (e: React.FormEvent) => { e.preventDefault(); if (editingNewsItem) { const isNew = !newsItems.find(n => n.id === editingNewsItem.id); if (isNew) { - dataStore.addNews(editingNewsItem); + await dataStore.addNews(editingNewsItem); } else { - dataStore.updateNews(editingNewsItem.id, editingNewsItem); + await dataStore.updateNews(editingNewsItem.id, editingNewsItem); } loadData(); setShowNewsModal(false); @@ -301,14 +318,14 @@ export default function Dashboard() { setShowMediaModal(true); }; - const handleSaveMedia = (e: React.FormEvent) => { + const handleSaveMedia = async (e: React.FormEvent) => { e.preventDefault(); if (editingMediaItem) { const isNew = !mediaItems.find(m => m.id === editingMediaItem.id); if (isNew) { - dataStore.addMedia(editingMediaItem); + await dataStore.addMedia(editingMediaItem); } else { - dataStore.updateMedia(editingMediaItem.id, editingMediaItem); + await dataStore.updateMedia(editingMediaItem.id, editingMediaItem); } loadData(); setShowMediaModal(false); @@ -337,14 +354,14 @@ export default function Dashboard() { setShowDocumentModal(true); }; - const handleSaveDocument = (e: React.FormEvent) => { + const handleSaveDocument = async (e: React.FormEvent) => { e.preventDefault(); if (editingDocument) { const isNew = !documents.find(d => d.id === editingDocument.id); if (isNew) { - dataStore.addDocument(editingDocument); + await dataStore.addDocument(editingDocument); } else { - dataStore.updateDocument(editingDocument.id, editingDocument); + await dataStore.updateDocument(editingDocument.id, editingDocument); } loadData(); setShowDocumentModal(false); @@ -358,10 +375,10 @@ export default function Dashboard() { setShowStationModal(true); }; - const handleSaveStation = (e: React.FormEvent) => { + const handleSaveStation = async (e: React.FormEvent) => { e.preventDefault(); if (editingStation) { - dataStore.updateStation(editingStation.id, editingStation); + await dataStore.updateStation(editingStation.id, editingStation); loadData(); setShowStationModal(false); setEditingStation(null); @@ -1170,147 +1187,105 @@ export default function Dashboard() {

Metro Hattı İstasyonları

-

İstasyon durumlarını ve ilerlemeyi yönetin

+

Hangi istasyonun inşaatta olduğunu seçin

-
- {/* Status Filter */} + {/* Basit İstasyon Listesi */}
-
- - - - -
-
+
+ {metroStations.map((station, index) => { + const selectedIndex = selectedStationId + ? metroStations.findIndex(s => s.id === selectedStationId) + : -1; + + let status = 'planned'; + if (selectedIndex !== -1) { + if (index < selectedIndex) status = 'completed'; + else if (index === selectedIndex) status = 'inprogress'; + } - {/* Stations Grid */} -
- {filteredStationsList.map((station) => ( -
handleStationClick(station.id)} - className={`bg-white rounded-xl shadow-sm overflow-hidden hover:shadow-lg transition-all cursor-pointer ${ - selectedStationId === station.id ? 'ring-4 ring-[#00B4D8]' : '' - }`} - > -
-
-
-

{station.name}

- - {getStatusText(station.status)} - -
-
- {selectedStationId === station.id && ( -
- - - + return ( +
handleStationClick(station.id)} + className={` + flex items-center gap-4 p-4 rounded-xl cursor-pointer transition-all + ${status === 'completed' ? 'bg-green-50 border-2 border-green-200' : ''} + ${status === 'inprogress' ? 'bg-blue-50 border-2 border-blue-300 shadow-lg' : ''} + ${status === 'planned' ? 'bg-gray-50 border-2 border-gray-200' : ''} + ${selectedStationId === station.id ? 'ring-4 ring-blue-400' : ''} + hover:shadow-md + `} + > + {/* İkon */} +
+ {status === 'completed' && ( +
+ ✓
)} - -
-
- - {/* Progress Bar */} -
-
- İlerleme - {station.progress}% -
-
-
-
-
- - {/* Details */} -
-
- 📅 Başlangıç: - {station.startDate} -
-
- 🎯 Bitiş: - {station.expectedCompletion} -
-
- 👥 Günlük Kapasite: - {station.dailyCapacity} yolcu -
- {station.connections && station.connections.length > 0 && ( -
- 🔄 Aktarma: -
- {station.connections.map((conn, i) => ( - - {conn} - - ))} + {status === 'inprogress' && ( +
+ 🚧
-
- )} + )} + {status === 'planned' && ( +
+ ○ +
+ )} +
+ + {/* Durak Bilgileri */} +
+

+ {station.name} +

+ {station.district && ( +

+ 📍 {station.district} +

+ )} +
+ + {/* Durum Badge */} +
+ {status === 'completed' && ( + + ✓ Yapıldı + + )} + {status === 'inprogress' && ( + + 🚧 Yapılıyor + + )} + {status === 'planned' && ( + + 📋 Planlı + + )} +
-
+ ); + })} +
+ + {/* Açıklama */} +
+
+

+ 💡 Nasıl Çalışır: Şu an inşaatta olan istasyonu tıklayın. + Seçtiğiniz istasyondan öncekiler "Yapıldı", seçtiğiniz "Yapılıyor", sonrakiler "Planlı" olarak gösterilir. +

- ))} +
)} @@ -1324,10 +1299,11 @@ export default function Dashboard() {

YouTube canlı yayın URL'sini ve ayarlarını yönetin

-
{ + { e.preventDefault(); - dataStore.setLiveStream(liveStreamConfig); + await dataStore.setLiveStream(liveStreamConfig); alert('Canlı yayın ayarları kaydedildi!'); + loadData(); }} className="space-y-6">
@@ -1362,7 +1338,7 @@ export default function Dashboard() { type="url" value={liveStreamConfig.url} onChange={(e) => setLiveStreamConfig({ ...liveStreamConfig, url: 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" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#00B4D8] focus:border-transparent text-gray-900" placeholder="https://www.youtube.com/embed/VIDEO_ID" required /> @@ -1474,9 +1450,9 @@ export default function Dashboard() { )}
{ - dataStore.updateFAQ(faq.id, { order: parseInt(e.target.value) }); + onChange={async (e) => { + await dataStore.updateFAQ(faq.id, { order: parseInt(e.target.value) }); loadData(); }} className="w-16 px-2 py-2 border border-gray-300 rounded-lg text-center" title="Sıra" />