update deploy

This commit is contained in:
bulut
2026-03-27 10:41:54 +03:00
parent 69d19c0176
commit 6f6448aa06
422 changed files with 37956 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
# Admin Panel — Eğitim Blokları (Course Blocks)
## Genel Bakış
Eğitim detay sayfalarında artık **Page Builder** mantığı var. Her eğitimin altına sıralı bloklar eklenebilir. Yapı, Page Blocks ile birebir aynı — aynı blok tipleri, aynı `_width` desteği, aynı content JSON formatı.
---
## API Endpoints
Tüm endpoint'ler `auth:sanctum` ile korunuyor. Base URL: `{API_URL}/api/admin`
| Method | Endpoint | Açıklama |
|--------|----------|----------|
| `GET` | `/admin/courses/{course}/blocks` | Eğitimin bloklarını listele |
| `POST` | `/admin/courses/{course}/blocks` | Yeni blok oluştur |
| `GET` | `/admin/courses/{course}/blocks/{block}` | Blok detayı |
| `PUT` | `/admin/courses/{course}/blocks/{block}` | Blok güncelle |
| `DELETE` | `/admin/courses/{course}/blocks/{block}` | Blok sil |
| `POST` | `/admin/courses/{course}/blocks/reorder` | Sıralama güncelle |
---
## GET /admin/courses/{course}/blocks
```json
{
"data": [
{
"id": 1,
"type": "hero",
"content": {
"title": "Köprüüstü Kaynak Yönetimi",
"subtitle": "BRM",
"description": "STCW uyumlu ileri düzey eğitim..."
},
"order_index": 0
},
{
"id": 2,
"type": "text",
"content": {
"_width": "half",
"title": "Eğitim Kapsamı",
"body": "<ul><li>Liderlik</li><li>İletişim</li></ul>"
},
"order_index": 1
}
]
}
```
---
## POST /admin/courses/{course}/blocks — Yeni Blok
### Request:
```json
{
"type": "hero",
"content": {
"title": "Blok başlığı",
"description": "Açıklama..."
},
"order_index": 0,
"is_active": true
}
```
### Response (201):
```json
{
"id": 5,
"type": "hero",
"content": { "title": "Blok başlığı", "description": "Açıklama..." },
"order_index": 0
}
```
### Validation Kuralları:
| Alan | Kural |
|------|-------|
| `type` | required, string, max:50 |
| `content` | present, array (boş obje `{}` gönderilebilir) |
| `order_index` | optional, integer, min:0 (gönderilmezse otomatik son sıraya eklenir) |
| `is_active` | optional, boolean |
---
## PUT /admin/courses/{course}/blocks/{block} — Güncelle
Sadece değişen alanları gönder:
```json
{
"content": {
"_width": "half",
"title": "Güncel Başlık",
"body": "<p>Yeni içerik</p>"
}
}
```
---
## DELETE /admin/courses/{course}/blocks/{block}
```json
{ "message": "Blok silindi." }
```
---
## POST /admin/courses/{course}/blocks/reorder — Sıralama
```json
{
"items": [
{ "id": 3, "order_index": 0 },
{ "id": 1, "order_index": 1 },
{ "id": 2, "order_index": 2 }
]
}
```
```json
{ "message": "Blok sıralaması güncellendi." }
```
---
## Blok Tipleri
Page Blocks ile aynı blok tipleri kullanılır:
| type | Açıklama | Örnek Kullanım |
|------|----------|----------------|
| `hero` | Üst banner/başlık alanı | Eğitim hero bölümü |
| `text` | Zengin metin bloğu | Eğitim kapsamı, açıklama |
| `text_image` | Metin + görsel yan yana | Eğitim tanıtımı |
| `cards` | Kart grid | Özellikler, sertifikalar |
| `stats_grid` | İstatistik/adım kartları | Süre, katılımcı, başarı oranı |
| `cta` | Call-to-action | Kayıt ol butonu |
| `faq` | Sıkça sorulan sorular | Eğitimle ilgili SSS |
| `gallery` | Görsel galeri | Eğitim ortamı fotoğrafları |
| `video` | Video embed | Tanıtım videosu |
| `testimonials` | Yorumlar/referanslar | Mezun görüşleri |
| `html` | Serbest HTML | Özel içerik |
---
## `_width` Desteği
`content` JSON içinde `_width` key'i blok genişliğini belirler:
| Değer | Açıklama |
|-------|----------|
| `"full"` | Tam genişlik (varsayılan — key gönderilmezse otomatik full) |
| `"half"` | Yarım genişlik — ardışık iki half blok yan yana render edilir |
```json
// Yan yana iki blok
{ "type": "text", "content": { "_width": "half", "title": "Sol", "body": "..." }, "order_index": 0 }
{ "type": "stats_grid", "content": { "_width": "half", "title": "Sağ", ... }, "order_index": 1 }
```
---
## Public API — Frontend
`GET /api/v1/courses/{slug}` artık `blocks` array'ini de döner:
```json
{
"data": {
"id": 1,
"slug": "kopruustu-kaynak-yonetimi",
"title": "Köprüüstü Kaynak Yönetimi (BRM)",
"category": { ... },
"blocks": [
{ "id": 1, "type": "hero", "content": { ... }, "order_index": 0 },
{ "id": 2, "type": "text", "content": { "_width": "half", ... }, "order_index": 1 },
{ "id": 3, "type": "stats_grid", "content": { "_width": "half", ... }, "order_index": 2 }
],
"schedules": [ ... ],
...
}
}
```
Frontend, sayfa blokları ile aynı `BlockRenderer` bileşenini kullanabilir.
---
## Admin Panel Entegrasyonu
### Önerilen UI:
Eğitim düzenleme sayfasında (`/admin/courses/{id}/edit`) mevcut form alanlarının altına bir **"Bloklar"** sekmesi/bölümü ekle. Bu bölüm Page Builder ile aynı mantıkta çalışır:
1. Blok listesi `order_index` sıralı gösterilir
2. Sürükle-bırak ile sıralama → `POST .../reorder`
3. "Blok Ekle" butonu → tip seçimi → `POST .../blocks`
4. Blok düzenleme → inline edit veya modal → `PUT .../blocks/{id}`
5. Blok silme → onay dialog → `DELETE .../blocks/{id}`
### Aynı bileşenleri paylaşabilirsin:
Page Blocks için yazdığın `BlockEditor`, `BlockTypeSelector`, `ContentEditor` bileşenlerini **doğrudan** course blocks için de kullan. Sadece API endpoint prefix'i değişir:
- Page: `/admin/pages/{id}/blocks`
- Course: `/admin/courses/{id}/blocks`

246
prompts/admin-leads.md Normal file
View File

@@ -0,0 +1,246 @@
# Admin Panel — Leads (Başvuru Yönetimi) Modülü
## Genel Bakış
Web sitesindeki formlardan gelen tüm başvurular `leads` tablosuna yazılır. Admin panelden başvurular listelenir, detay görüntülenir, durum güncellenir ve not eklenir. **Lead'ler sadece API'den oluşur — admin panelde "Yeni Oluştur" butonu olmayacak.**
---
## API Endpoints (Admin — auth:sanctum)
| Method | Endpoint | Açıklama |
|--------|----------|----------|
| `GET` | `/admin/leads` | Başvuruları listele (paginated, filtrelenebilir) |
| `GET` | `/admin/leads/{id}` | Başvuru detayı (otomatik okundu işaretler) |
| `PUT` | `/admin/leads/{id}` | Durum/not güncelle |
| `DELETE` | `/admin/leads/{id}` | Başvuru sil (soft delete) |
---
## GET /admin/leads — Liste
### Query Parametreleri:
| Parametre | Tip | Açıklama |
|-----------|-----|----------|
| `status` | string | Durum filtresi: `new`, `contacted`, `enrolled`, `cancelled` |
| `source` | string | Kaynak filtresi: `kurs_kayit`, `danismanlik`, `duyuru`, `iletisim` |
| `is_read` | boolean | Okundu/okunmadı filtresi |
| `search` | string | İsim/telefon arama |
| `per_page` | integer | Sayfa başına (varsayılan: 15) |
### Response:
```json
{
"data": [
{
"id": 1,
"name": "Ahmet Yılmaz",
"phone": "+90 532 724 15 32",
"email": "ahmet@email.com",
"source": "kurs_kayit",
"status": "new",
"target_course": "gemici-birlesik-egitimi",
"education_level": "lise",
"subject": null,
"message": "Eğitim hakkında bilgi almak istiyorum",
"is_read": false,
"kvkk_consent": true,
"marketing_consent": false,
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "denizcilik-2026",
"admin_note": null,
"created_at": "2026-03-24T10:30:00.000000Z",
"updated_at": "2026-03-24T10:30:00.000000Z"
}
],
"meta": { "current_page": 1, "last_page": 5, "per_page": 15, "total": 72 }
}
```
---
## GET /admin/leads/{id} — Detay
Detay açıldığında backend otomatik olarak `is_read: true` yapar.
---
## PUT /admin/leads/{id} — Güncelle
Admin sadece şu alanları güncelleyebilir:
```json
{
"status": "contacted",
"is_read": true,
"admin_note": "Arandı, bilgi verildi. Nisan dönemine kayıt olacak."
}
```
---
## Veri Modeli
### Lead Alanları
| Alan | Tip | Açıklama |
|------|-----|----------|
| `id` | integer | — |
| `name` | string | Ad Soyad |
| `phone` | string | Telefon |
| `email` | string (nullable) | E-posta |
| `source` | enum | Başvuru kaynağı (form tipi) |
| `status` | enum | İşlem durumu |
| `target_course` | string (nullable) | Hedef eğitim slug'ı |
| `education_level` | string (nullable) | Eğitim seviyesi |
| `subject` | string (nullable) | Konu (iletişim formunda) |
| `message` | text (nullable) | Mesaj |
| `is_read` | boolean | Admin tarafından okundu mu |
| `kvkk_consent` | boolean | KVKK onayı |
| `marketing_consent` | boolean | Pazarlama onayı |
| `utm_source` | string (nullable) | UTM Source |
| `utm_medium` | string (nullable) | UTM Medium |
| `utm_campaign` | string (nullable) | UTM Campaign |
| `admin_note` | text (nullable) | Admin notu |
| `created_at` | datetime | Başvuru tarihi |
### Source (Kaynak) Değerleri
| Değer | Açıklama | Hangi Form? |
|-------|----------|-------------|
| `kurs_kayit` | Kurs ön kayıt | Eğitim detay sayfası kayıt formu |
| `danismanlik` | Danışmanlık | Danışmanlık sayfası formu |
| `duyuru` | Duyuru | Duyuru sidebar'daki mini form |
| `iletisim` | İletişim | İletişim sayfası formu |
| `hero_form` | Hero form | Anasayfa hero bölümü formu |
| `whatsapp_widget` | WhatsApp | WhatsApp widget üzerinden gelen |
### Status (Durum) Değerleri
| Değer | Label | Badge Rengi | Açıklama |
|-------|-------|-------------|----------|
| `new` | Yeni | `warning` (sarı) | Henüz işlenmemiş |
| `contacted` | İletişim Kuruldu | `info` (mavi) | Aranmış/e-posta gönderilmiş |
| `enrolled` | Kayıt Oldu | `success` (yeşil) | Eğitime kaydolmuş |
| `cancelled` | İptal | `danger` (kırmızı) | Vazgeçmiş/iptal |
---
## Önerilen Admin Panel Sayfası
### Liste Sayfası (`/admin/leads`)
```
┌─────────────────────────────────────────────────────────────┐
│ Başvurular [Filtreler ▼] │
├─────────────────────────────────────────────────────────────┤
│ Filtreler: [Kaynak ▼] [Durum ▼] [Okundu ▼] [Ara...] │
├──────┬──────────────┬──────────────┬────────────┬───────────┤
│ ● │ Ahmet Yılmaz │ +90 532 ... │ kurs_kayit │ 🟡 Yeni │
│ ○ │ Mehmet Kaya │ +90 555 ... │ iletisim │ 🔵 İlet. │
│ ○ │ Ayşe Demir │ +90 212 ... │ danismanlik│ 🟢 Kayıt │
└──────┴──────────────┴──────────────┴────────────┴───────────┘
● = okunmadı (bold göster) ○ = okundu
```
#### Tablo Sütunları:
| Sütun | Açıklama |
|-------|----------|
| Okundu göstergesi | `is_read` false ise bold/dot göster |
| Ad Soyad | `name` |
| Telefon | `phone` |
| E-posta | `email` (varsa) |
| Kaynak | `source` — badge ile göster |
| Hedef Eğitim | `target_course` (varsa) |
| Durum | `status` — renkli badge |
| Tarih | `created_at` — relative (2 saat önce) |
#### Kaynak Badge Renkleri:
| Source | Renk |
|--------|------|
| `kurs_kayit` | `primary` (mavi) |
| `danismanlik` | `purple` |
| `duyuru` | `orange` |
| `iletisim` | `teal` |
| `hero_form` | `indigo` |
| `whatsapp_widget` | `green` |
### Detay/Düzenleme Sayfası (`/admin/leads/{id}`)
```
┌─────────────────────────────────────────────────┐
│ Başvuru #42 — Ahmet Yılmaz │
├─────────────────────────────────────────────────┤
│ Bilgiler │
│ ┌─────────────────┬───────────────────────────┐ │
│ │ Ad Soyad │ Ahmet Yılmaz │ │
│ │ Telefon │ +90 532 724 15 32 │ │
│ │ E-posta │ ahmet@email.com │ │
│ │ Kaynak │ 🔵 kurs_kayit │ │
│ │ Hedef Eğitim │ Gemici (Birleşik) Eğitimi │ │
│ │ Eğitim Seviyesi │ Lise │ │
│ │ Mesaj │ Bilgi almak istiyorum │ │
│ │ KVKK Onay │ ✅ Evet │ │
│ │ Pazarlama Onay │ ❌ Hayır │ │
│ │ Tarih │ 24 Mar 2026, 10:30 │ │
│ └─────────────────┴───────────────────────────┘ │
│ │
│ UTM Bilgileri │
│ ┌─────────────────┬───────────────────────────┐ │
│ │ utm_source │ google │ │
│ │ utm_medium │ cpc │ │
│ │ utm_campaign │ denizcilik-2026 │ │
│ └─────────────────┴───────────────────────────┘ │
│ │
│ İşlem │
│ ┌───────────────────────────────────────────────┐│
│ │ Durum: [Yeni ▼] → contacted / enrolled /.. ││
│ │ ││
│ │ Admin Notu: ││
│ │ ┌─────────────────────────────────────────┐ ││
│ │ │ Arandı, bilgi verildi. Nisan dönemine │ ││
│ │ │ kayıt olacak. │ ││
│ │ └─────────────────────────────────────────┘ ││
│ │ ││
│ │ [Kaydet] ││
│ └───────────────────────────────────────────────┘│
└─────────────────────────────────────────────────┘
```
#### Düzenleme Kuralları:
- Başvuru bilgileri (name, phone, message vb.) **readonly** — sadece görüntüleme
- Sadece `status`, `is_read` ve `admin_note` düzenlenebilir
- `target_course` slug ise, linke çevir: `/admin/courses/{slug}/edit`
- "Yeni Oluştur" butonu **yok** — lead'ler sadece frontend formlarından gelir
---
## İstatistik Kartları (Dashboard için, opsiyonel)
Liste sayfasının üstüne istatistik kartları eklenebilir:
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 🟡 Yeni │ │ 🔵 İletişim │ │ 🟢 Kayıt │ │ 📊 Toplam │
│ 12 │ │ 28 │ │ 45 │ │ 93 │
│ Bu hafta: 5 │ │ Bu hafta: 8 │ │ Bu ay: 15 │ │ Bu ay: 32 │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
```
API'den alınabilir:
```
GET /admin/leads?status=new → meta.total
GET /admin/leads?status=contacted → meta.total
GET /admin/leads?status=enrolled → meta.total
GET /admin/leads → meta.total
```
---
## Notlar
- `is_read` detay sayfasııldığında otomatik `true` olur (backend tarafından)
- Soft delete kullanılıyor — silinen lead'ler geri alınabilir
- UTM verileri backend'de tek JSON sütununda (`utm`) saklanır, API response'da ayrı ayrı döner
- KVKK ve pazarlama onayları readonly gösterilmeli — admin tarafından değiştirilemez

375
prompts/admin-settings.md Normal file
View File

@@ -0,0 +1,375 @@
# Admin Panel — Site Ayarları Modülü
## Genel Bakış
Backend'de `settings` tablosu güncellendi. Artık her ayarın **label** (Türkçe etiket) ve **order_index** (sıralama) alanları var. Ayarlar **9 gruba** ayrılmış durumda. Admin panelde her grup kendi sekmesi/sayfası olacak.
---
## API Endpoints
Tüm admin endpoint'leri `auth:sanctum` ile korunuyor. Base URL: `{API_URL}/api/admin`
| Method | Endpoint | Açıklama |
|--------|----------|----------|
| `GET` | `/admin/settings` | Tüm ayarları flat liste olarak getir |
| `GET` | `/admin/settings/group/{group}` | Tek grup ayarlarını getir |
| `PUT` | `/admin/settings` | Toplu güncelleme (dot notation) |
| `POST` | `/admin/settings/clear-cache` | Ayar cache'ini temizle |
---
## GET /admin/settings — Tüm Ayarlar
Her ayar şu formatta döner:
```json
{
"data": [
{
"id": 1,
"key": "site_name",
"value": "Boğaziçi Denizcilik Eğitim Kurumu",
"group": "general",
"type": "text",
"label": "Site Adı",
"order_index": 0
},
{
"id": 8,
"key": "announcement_bar_active",
"value": "true",
"group": "general",
"type": "boolean",
"label": "Üst Bar Aktif mi",
"order_index": 7
}
]
}
```
**Frontend'de `group` bazında filtrele** → Her sekme/tab kendi grubunu gösterir.
---
## GET /admin/settings/group/{group} — Tek Grup
Sadece o grubun ayarlarını döner. `order_index` sıralı gelir.
Geçerli group değerleri:
- `general` — Genel site ayarları
- `contact` — İletişim bilgileri
- `maps` — Harita ayarları
- `social` — Sosyal medya linkleri
- `seo` — SEO, Open Graph, Twitter Card, doğrulama kodları
- `analytics` — Google Analytics, Tag Manager, Pixel ID'leri
- `header` — Navbar ve üst bar ayarları
- `footer` — Footer içerik ve stili
- `integrations` — SMTP, reCAPTCHA, bildirim ayarları
---
## PUT /admin/settings — Toplu Güncelleme
**Dot notation** formatında gönder: `{group}.{key}: value`
### Request Body:
```json
{
"settings": {
"general.site_name": "Yeni Site Adı",
"general.announcement_bar_active": "true",
"contact.phone_primary": "+90 555 123 45 67",
"social.instagram_url": "https://instagram.com/bogazicidenizcilik",
"header.cta_button_text": "Kayıt Ol",
"footer.copyright_text": "© 2026 Boğaziçi Denizcilik"
}
}
```
### Response:
```json
{ "message": "Ayarlar güncellendi." }
```
### Önemli:
- Sadece değişen ayarları gönderin, hepsini göndermenize gerek yok
- PUT sonrası backend otomatik olarak tüm cache'leri temizler
- Validation: `settings` required array, her value nullable string
---
## POST /admin/settings/clear-cache
Body gerekmez. Manuel cache temizleme butonu için.
```json
{ "message": "Ayar cache temizlendi." }
```
---
## Type'lara Göre Form Bileşenleri
Her ayarın `type` alanı, admin panelde hangi input bileşeninin kullanılacağını belirler:
| type | Bileşen | Açıklama |
|------|---------|----------|
| `text` | `<input type="text">` | Tek satır metin |
| `textarea` | `<textarea>` | Çok satır metin |
| `image` | Dosya yükleme + önizleme | Mevcut upload endpoint'i kullanılır (`POST /admin/uploads`) |
| `boolean` | Toggle/Switch | value `"true"` veya `"false"` string olarak saklanır |
| `url` | `<input type="url">` | URL formatı |
| `color` | Color picker | Hex renk kodu (#RRGGBB) |
| `richtext` | TipTap / zengin metin editör | HTML içerik |
| `json` | JSON editör | Yapısal veri |
### Boolean Değerler
Backend'de `value` her zaman **string** olarak saklanır. Boolean'lar `"true"` / `"false"` string:
```js
// Gönderirken
{ "settings": { "general.maintenance_mode": "false" } }
// Okurken
setting.value === "true" // → toggle ON
setting.value === "false" // → toggle OFF
```
### Image Alanları
1. Dosyayı `POST /admin/uploads` ile yükle → response'dan URL al
2. URL'yi settings update'e gönder:
```json
{ "settings": { "general.logo_light": "/storage/uploads/logo-white.png" } }
```
---
## Grup Detayları ve Alanlar
### general (13 alan)
| key | type | label |
|-----|------|-------|
| site_name | text | Site Adı |
| site_tagline | text | Slogan |
| site_description | textarea | Kısa Site Açıklaması |
| logo_light | image | Logo — Açık Tema (beyaz navbar) |
| logo_dark | image | Logo — Koyu Tema (dark bg) |
| favicon | image | Favicon (32x32 PNG) |
| apple_touch_icon | image | Apple Touch Icon (180x180) |
| announcement_bar_active | boolean | Üst Bar Aktif mi |
| announcement_bar_text | text | Üst Bar Metni |
| announcement_bar_url | url | Üst Bar Linki |
| announcement_bar_bg_color | color | Üst Bar Arka Plan Rengi |
| maintenance_mode | boolean | Bakım Modu |
| maintenance_message | textarea | Bakım Modu Mesajı |
### contact (15 alan)
| key | type | label |
|-----|------|-------|
| phone_primary | text | Ana Telefon |
| phone_secondary | text | İkinci Telefon |
| email_info | text | Bilgi E-postası |
| email_support | text | Destek E-postası |
| email_kayit | text | Kayıt E-postası |
| address_full | textarea | Tam Adres |
| address_short | text | Kısa Adres (navbar için) |
| district | text | İlçe |
| city | text | Şehir |
| postal_code | text | Posta Kodu |
| working_hours_weekday | text | Hafta İçi Saatleri |
| working_hours_saturday | text | Cumartesi Saatleri |
| working_hours_sunday | text | Pazar Saatleri |
| whatsapp_number | text | WhatsApp (+90 ile başlayan) |
| whatsapp_message | text | WhatsApp Varsayılan Mesaj |
### maps (6 alan)
| key | type | label |
|-----|------|-------|
| google_maps_embed_url | textarea | Google Maps Embed URL (iframe src) |
| google_maps_place_url | url | Google Maps Profil Linki |
| google_maps_api_key | text | Google Maps API Key |
| latitude | text | Enlem |
| longitude | text | Boylam |
| map_zoom_level | text | Harita Zoom (1-20) |
> **Not:** `google_maps_api_key` admin endpoint'ten görünür ama public API'den filtrelenir.
### social (11 alan)
| key | type | label |
|-----|------|-------|
| instagram_url | url | Instagram Profil URL |
| instagram_handle | text | Instagram Kullanıcı Adı (@siz) |
| facebook_url | url | Facebook Sayfası URL |
| facebook_page_id | text | Facebook Page ID |
| twitter_url | url | X (Twitter) Profil URL |
| twitter_handle | text | X Kullanıcı Adı (@siz) |
| youtube_url | url | YouTube Kanal URL |
| youtube_channel_id | text | YouTube Channel ID |
| linkedin_url | url | LinkedIn Sayfa URL |
| tiktok_url | url | TikTok Profil URL |
| pinterest_url | url | Pinterest URL |
### seo (23 alan)
**Temel SEO:**
| key | type | label |
|-----|------|-------|
| meta_title_suffix | text | Title Eki |
| meta_title_separator | text | Ayraç Karakteri |
| default_meta_description | textarea | Varsayılan Meta Açıklama |
| default_meta_keywords | textarea | Varsayılan Keywords (virgülle) |
| robots | text | Robots |
| canonical_domain | url | Canonical Domain |
**Open Graph:**
| key | type | label |
|-----|------|-------|
| og_title | text | OG Default Title |
| og_description | textarea | OG Default Description |
| og_image | image | OG Default Görsel (1200x630 px) |
| og_type | text | OG Type |
| og_locale | text | OG Locale |
| og_site_name | text | OG Site Name |
| facebook_app_id | text | Facebook App ID |
**Twitter / X Card:**
| key | type | label |
|-----|------|-------|
| twitter_card_type | text | Card Tipi |
| twitter_site | text | Site @handle |
| twitter_creator | text | İçerik Sahibi @handle |
| twitter_title | text | Twitter Default Title |
| twitter_description | textarea | Twitter Default Description |
| twitter_image | image | Twitter Card Görseli (1200x600 px) |
**Doğrulama Kodları:**
| key | type | label |
|-----|------|-------|
| google_site_verification | text | Google Search Console Kodu |
| bing_site_verification | text | Bing Webmaster Kodu |
| yandex_verification | text | Yandex Webmaster Kodu |
| pinterest_verification | text | Pinterest Doğrulama Kodu |
> **UI Önerisi:** SEO grubunu 4 alt sekmeye bölebilirsin: Temel SEO, Open Graph, Twitter Card, Doğrulama Kodları.
### analytics (10 alan)
| key | type | label |
|-----|------|-------|
| google_analytics_id | text | Google Analytics 4 ID (G-XXXXXXXX) |
| google_tag_manager_id | text | Google Tag Manager ID (GTM-XXXXXXX) |
| google_ads_id | text | Google Ads Conversion ID |
| facebook_pixel_id | text | Meta (Facebook) Pixel ID |
| hotjar_id | text | Hotjar Site ID |
| clarity_id | text | Microsoft Clarity ID |
| tiktok_pixel_id | text | TikTok Pixel ID |
| crisp_website_id | text | Crisp Chat Website ID |
| custom_head_scripts | textarea | `<head>` içine özel script |
| custom_body_scripts | textarea | `<body>` sonuna özel script |
### header (9 alan)
| key | type | label |
|-----|------|-------|
| navbar_style_default | text | Varsayılan Navbar Stili (transparent/white) |
| cta_button_text | text | Sağ Üst Buton Metni |
| cta_button_url | url | Sağ Üst Buton Linki |
| cta_button_color | color | Sağ Üst Buton Rengi |
| show_phone_topbar | boolean | Üst Bar'da Telefon Göster |
| show_email_topbar | boolean | Üst Bar'da E-posta Göster |
| show_address_topbar | boolean | Üst Bar'da Adres Göster |
| show_hours_topbar | boolean | Üst Bar'da Saat Göster |
| show_social_navbar | boolean | Navbar'da Sosyal Medya İkonları Göster |
### footer (8 alan)
| key | type | label |
|-----|------|-------|
| footer_description | textarea | Footer Açıklaması |
| footer_logo | image | Footer Logo (varsa ayrı) |
| copyright_text | text | Copyright Metni |
| footer_address | textarea | Footer Adres |
| footer_phone | text | Footer Telefon |
| footer_email | text | Footer E-posta |
| footer_bg_color | color | Footer Arka Plan Rengi |
| show_social_footer | boolean | Footer'da Sosyal Medya Göster |
### integrations (10 alan)
| key | type | label |
|-----|------|-------|
| recaptcha_site_key | text | reCAPTCHA v3 Site Key |
| recaptcha_secret_key | text | reCAPTCHA v3 Secret Key |
| smtp_host | text | SMTP Host |
| smtp_port | text | SMTP Port |
| smtp_username | text | SMTP Kullanıcı Adı |
| smtp_password | text | SMTP Şifre |
| smtp_encryption | text | SMTP Şifreleme (tls/ssl) |
| smtp_from_name | text | Mail Gönderen Adı |
| smtp_from_email | text | Mail Gönderen Adresi |
| notification_emails | textarea | Bildirim E-postaları (virgülle) |
> **Not:** `integrations` grubundaki tüm ayarlar `is_public: false` — public API'den hiçbiri görünmez. `smtp_password` ve `*_secret_key` alanları password input olarak gösterilmeli.
---
## Önerilen Admin Panel Sayfa Yapısı
```
/admin/settings
├── Sidebar veya Tab Navigation
│ ├── Genel → general
│ ├── İletişim → contact
│ ├── Harita → maps
│ ├── Sosyal Medya → social
│ ├── SEO → seo (alt sekmeler: Temel, OG, Twitter, Doğrulama)
│ ├── Analitik → analytics
│ ├── Header → header
│ ├── Footer → footer
│ └── Entegrasyonlar → integrations
```
### Kaydet Akışı:
1. Kullanıcı bir gruptaki alanları düzenler
2. "Kaydet" butonuna basar
3. Sadece **değişen** alanlar `PUT /admin/settings` ile gönderilir
4. Başarılı response sonrası toast mesaj göster
5. Gerekirse `POST /admin/settings/clear-cache` butonu ekle (teknik kullanıcılar için)
### Dinamik Form Render:
`label` alanını form label olarak, `type` alanını input bileşeni seçmek için kullan. Backend'den gelen sıralama (`order_index`) form alanlarının sırası olarak kullanılabilir.
---
## Preview Sistemi (Page Builder)
Admin panelden sayfa blokları düzenlenirken önizleme özelliği:
| Method | Endpoint | Açıklama |
|--------|----------|----------|
| `POST` | `/admin/preview` | Önizleme token'ı oluştur |
| `DELETE` | `/admin/preview/{token}` | Önizlemeyi sil |
| `GET` | `/v1/preview/{token}` | Public — frontend fetch (auth yok) |
### POST /admin/preview
```json
// Request
{
"page_id": 1,
"blocks": [
{ "type": "hero", "content": { "title": "...", "description": "..." }, "order_index": 0 },
{ "type": "text", "content": { "_width": "half", "title": "...", "body": "..." }, "order_index": 1 }
]
}
// Response (201)
{
"token": "550e8400-e29b-41d4-a716-446655440000",
"preview_url": "http://localhost:3000/api/preview?token=550e8400...&slug=kalite-politikasi",
"expires_in": 600
}
```
### Kullanım:
1. Admin panelde "Önizle" butonuna bas → `POST /admin/preview`
2. Response'daki `preview_url`'yi yeni sekmede aç
3. Önizleme 10 dakika geçerli, sonra otomatik silinir
4. İsteğe bağlı: modal kapatılınca `DELETE /admin/preview/{token}` ile temizle

View File

@@ -0,0 +1,220 @@
# Admin Panel — Stories + Info Sections
## 1. Stories (Hikayeler)
Anasayfadaki kayan hikaye/tanıtım kartları. Instagram stories benzeri yapı.
### API Endpoints
| Method | Endpoint | Auth | Açıklama |
|--------|----------|------|----------|
| `GET` | `/v1/stories` | public | Aktif hikayeler (order_index sıralı) |
| `GET` | `/admin/stories` | sanctum | Tüm hikayeler |
| `POST` | `/admin/stories` | sanctum | Yeni hikaye oluştur |
| `GET` | `/admin/stories/{id}` | sanctum | Hikaye detayı |
| `PUT` | `/admin/stories/{id}` | sanctum | Hikaye güncelle |
| `DELETE` | `/admin/stories/{id}` | sanctum | Hikaye sil |
### Response Formatı
```json
{
"data": [
{
"id": 1,
"title": "25 Yılı Aşkın Deneyim",
"badge": "Tanıtım",
"content": "1998'den bu yana 15.000+ denizci yetiştirdik...",
"image": "/storage/stories/hakkimizda.jpg",
"cta_text": "Hakkımızda",
"cta_url": "/kurumsal/hakkimizda",
"order_index": 0,
"is_active": true,
"created_at": "...",
"updated_at": "..."
}
]
}
```
### Veri Modeli
| Alan | Tip | Açıklama |
|------|-----|----------|
| `title` | string (zorunlu) | Hikaye başlığı |
| `badge` | string (nullable) | Üst etiket: "Tanıtım", "Yeni Dönem", vb. |
| `content` | text (zorunlu) | Hikaye içeriği |
| `image` | string (nullable) | Görsel path (storage/stories/) |
| `cta_text` | string (nullable) | Buton metni: "Hakkımızda", "Detaylı Bilgi" |
| `cta_url` | string (nullable) | Buton linki: "/kurumsal/hakkimizda" |
| `order_index` | integer | Sıralama (0'dan başlar) |
| `is_active` | boolean | Aktif/pasif |
### Admin Panel Form
```
┌─────────────────────────────────────────────────┐
│ Hikaye Düzenle │
├─────────────────────────────────────────────────┤
│ Başlık: [25 Yılı Aşkın Deneyim ] │
│ Etiket: [Tanıtım ] │
│ İçerik: ┌─────────────────────────────────┐ │
│ │ 1998'den bu yana 15.000+... │ │
│ └─────────────────────────────────┘ │
│ Görsel: [📷 Dosya Seç] hakkimizda.jpg │
│ Buton: [Hakkımızda ] [/kurumsal/hak..] │
│ Sıralama: [0] │
│ Aktif: [✅ Toggle] │
│ [Kaydet] │
└─────────────────────────────────────────────────┘
```
#### Form Alanları:
| Alan | Bileşen | Açıklama |
|------|---------|----------|
| title | TextInput | Zorunlu, max 255 |
| badge | TextInput | İsteğe bağlı, max 100 |
| content | Textarea | Zorunlu |
| image | FileUpload | storage/public/stories, preview göster |
| cta_text | TextInput | İsteğe bağlı, max 100 |
| cta_url | TextInput (url) | İsteğe bağlı, max 255 |
| order_index | NumberInput | Varsayılan 0 |
| is_active | Toggle | Varsayılan true |
#### Tablo Sütunları:
| Sütun | Açıklama |
|-------|----------|
| Görsel | Küçük thumbnail |
| Başlık | title |
| Etiket | badge (badge bileşeni) |
| Buton | cta_text (varsa göster) |
| Sıralama | order_index |
| Aktif | is_active (toggle/icon) |
---
## 2. Info Sections (Tanıtım Bölümleri)
Anasayfadaki 2 adet metin+görsel tanıtım bölümü. Settings tablosunda `info_sections` grubu altında saklanır.
### API Erişimi
Mevcut settings endpoint'inden gelir:
```
GET /api/v1/settings
GET /api/v1/settings/info_sections
```
### Response (settings/info_sections):
```json
{
"info_section_1_badge": "Neden Boğaziçi Denizcilik?",
"info_section_1_title": "Uluslararası Standartlarda Eğitim",
"info_section_1_body": "Uzun açıklama metni...",
"info_section_1_quote": "Denizcilik eğitiminde kalite...",
"info_section_1_quote_author": "Kpt. Murat Aydın, Kurucu",
"info_section_1_image": "/storage/uploads/info-1.jpg",
"info_section_2_badge": "Simülatör Destekli Eğitim",
"info_section_2_title": "Teoriden Pratiğe",
"info_section_2_body": "Uzun açıklama metni...",
"info_section_2_image": "/storage/uploads/info-2.jpg"
}
```
### Alanlar
**Bölüm 1 (6 alan):**
| Key | Type | Label |
|-----|------|-------|
| `info_section_1_badge` | text | Bölüm 1 — Etiket |
| `info_section_1_title` | text | Bölüm 1 — Başlık |
| `info_section_1_body` | textarea | Bölüm 1 — İçerik |
| `info_section_1_quote` | text | Bölüm 1 — Alıntı |
| `info_section_1_quote_author` | text | Bölüm 1 — Alıntı Yazarı |
| `info_section_1_image` | image | Bölüm 1 — Görsel |
**Bölüm 2 (4 alan):**
| Key | Type | Label |
|-----|------|-------|
| `info_section_2_badge` | text | Bölüm 2 — Etiket |
| `info_section_2_title` | text | Bölüm 2 — Başlık |
| `info_section_2_body` | textarea | Bölüm 2 — İçerik |
| `info_section_2_image` | image | Bölüm 2 — Görsel |
### Admin Panel — Ayarlar Sayfası
Mevcut Settings sayfasına yeni bir **"Tanıtım Bölümleri"** sekmesi ekle:
```
/admin/settings
├── ... (mevcut sekmeler)
└── Tanıtım Bölümleri → info_sections
├── Bölüm 1
│ ├── Etiket: [Neden Boğaziçi Denizcilik?]
│ ├── Başlık: [Uluslararası Standartlarda...]
│ ├── İçerik: [textarea — uzun metin]
│ ├── Alıntı: [Denizcilik eğitiminde kalite...]
│ ├── Alıntı Yazarı: [Kpt. Murat Aydın, Kurucu]
│ └── Görsel: [📷 FileUpload]
└── Bölüm 2
├── Etiket: [Simülatör Destekli Eğitim]
├── Başlık: [Teoriden Pratiğe]
├── İçerik: [textarea — uzun metin]
└── Görsel: [📷 FileUpload]
```
Kaydet: `PUT /admin/settings` ile dot notation:
```json
{
"settings": {
"info_sections.info_section_1_badge": "Neden Boğaziçi Denizcilik?",
"info_sections.info_section_1_title": "Uluslararası Standartlarda Eğitim",
"info_sections.info_section_1_body": "...",
"info_sections.info_section_1_image": "/storage/uploads/info-1.jpg"
}
}
```
---
## Frontend Kullanım Özeti
### Stories
```tsx
// Anasayfa
const { data: stories } = await fetch('/api/v1/stories');
{stories.map(story => (
<StoryCard
key={story.id}
title={story.title}
badge={story.badge}
content={story.content}
image={getImageUrl(story.image)}
ctaText={story.cta_text}
ctaUrl={story.cta_url}
/>
))}
```
### Info Sections
```tsx
// Settings'ten al
const settings = await fetch('/api/v1/settings');
const info1 = {
badge: settings.info_sections?.info_section_1_badge,
title: settings.info_sections?.info_section_1_title,
body: settings.info_sections?.info_section_1_body,
quote: settings.info_sections?.info_section_1_quote,
quoteAuthor: settings.info_sections?.info_section_1_quote_author,
image: settings.info_sections?.info_section_1_image,
};
// Bölüm 1 — görsel solda, metin sağda
<InfoSection {...info1} imagePosition="left" />
// Bölüm 2 — görsel sağda, metin solda (reverse)
<InfoSection {...info2} imagePosition="right" />
```

View File

@@ -0,0 +1,629 @@
# Frontend (Next.js) — API Entegrasyon Rehberi
## Genel Bilgi
Backend: Laravel API, Base URL: `{API_URL}/api/v1`
Auth gerektirmeyen tüm public endpoint'ler burada. Frontend SSR/ISR ile fetch edebilir.
---
## Tüm Public Endpoint'ler
| Method | Endpoint | Açıklama |
|--------|----------|----------|
| `GET` | `/v1/settings` | Site ayarları (nested group format) |
| `GET` | `/v1/settings/{group}` | Tek grup ayarları |
| `GET` | `/v1/categories` | Eğitim kategorileri |
| `GET` | `/v1/categories/{slug}` | Kategori detayı |
| `GET` | `/v1/courses` | Eğitim listesi (paginated) |
| `GET` | `/v1/courses/{slug}` | Eğitim detayı (blocks + schedules dahil) |
| `GET` | `/v1/schedules` | Eğitim takvimi |
| `GET` | `/v1/schedules/upcoming` | Yaklaşan eğitimler |
| `GET` | `/v1/schedules/{id}` | Takvim detayı |
| `GET` | `/v1/announcements` | Duyurular |
| `GET` | `/v1/announcements/{slug}` | Duyuru detayı |
| `GET` | `/v1/hero-slides` | Hero slider (aktif, sıralı) |
| `GET` | `/v1/faqs` | SSS listesi |
| `GET` | `/v1/faqs/{category}` | Kategoriye göre SSS |
| `GET` | `/v1/guide-cards` | Eğitim rehberi kartları |
| `GET` | `/v1/menus/{location}` | Menü (header, footer, vb.) |
| `GET` | `/v1/pages/{slug}` | Sayfa detayı (blocks dahil) |
| `GET` | `/v1/preview/{token}` | Sayfa önizleme (cache bazlı) |
| `GET` | `/v1/comments/{type}/{id}` | Yorumlar |
| `POST` | `/v1/leads` | Lead/başvuru formu |
| `POST` | `/v1/comments` | Yorum gönder |
| `GET` | `/v1/sitemap-data` | Sitemap verisi |
---
## Eğitimler (Courses)
### GET /v1/courses — Liste
Query parametreleri:
- `category` — Kategori slug filtresi (ör: `guverte`, `stcw`, `makine`)
- `search` — Başlık/açıklama arama
- `sort` — Sıralama: `title`, `-created_at` (varsayılan), `students`, `rating` (- prefix = desc)
- `per_page` — Sayfa başına (varsayılan: 15)
```
GET /v1/courses?category=guverte&sort=-rating&per_page=12
```
Response (paginated):
```json
{
"data": [
{
"id": 1,
"category_id": 1,
"category": { "id": 1, "slug": "guverte", "label": "Güverte Eğitimleri" },
"slug": "gemici-birlesik-egitimi",
"title": "Gemici (Birleşik) Eğitimi",
"sub": "STCW / IMO Uyumlu",
"desc": "Güverte bölümünde gemici olarak görev yapmak isteyen...",
"long_desc": "Detaylııklama...",
"duration": "32 Gün",
"students": 772,
"rating": 4.9,
"badge": "most_preferred",
"badge_label": "En Çok Tercih Edilen",
"image": null,
"price": "₺14.500",
"includes": ["Basılı eğitim materyalleri", "Uygulamalı güverte tatbikatları", "..."],
"requirements": ["En az 16 yaşında olmak", "..."],
"scope": ["Denizde kişisel güvenlik", "Yangınla mücadele", "..."],
"standard": "STCW / IMO Uyumlu",
"language": "Türkçe",
"location": "Kadıköy, İstanbul",
"meta_title": "Gemici (Birleşik) Eğitimi | Boğaziçi Denizcilik",
"meta_description": "STCW A-II/4 uyumlu...",
"created_at": "2026-03-02T...",
"updated_at": "2026-03-23T..."
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "last_page": 3, "per_page": 12, "total": 32 }
}
```
### GET /v1/courses/{slug} — Detay
Detayda ek olarak `blocks` ve `schedules` array'leri gelir:
```json
{
"data": {
"id": 1,
"slug": "gemici-birlesik-egitimi",
"title": "Gemici (Birleşik) Eğitimi",
"sub": "STCW / IMO Uyumlu",
"desc": "...",
"long_desc": "...",
"duration": "32 Gün",
"students": 772,
"rating": 4.9,
"badge": "most_preferred",
"badge_label": "En Çok Tercih Edilen",
"image": null,
"price": "₺14.500",
"includes": ["Basılı eğitim materyalleri", "..."],
"requirements": ["En az 16 yaşında olmak", "..."],
"scope": ["Denizde kişisel güvenlik", "..."],
"standard": "STCW / IMO Uyumlu",
"language": "Türkçe",
"location": "Kadıköy, İstanbul",
"category": { "id": 1, "slug": "guverte", "label": "Güverte Eğitimleri" },
"blocks": [
{ "id": 1, "type": "text", "content": { "label": "EĞİTİM HAKKINDA", "title": "Neden Bu Eğitim?", "body": "<p>...</p>" }, "order_index": 0 },
{ "id": 2, "type": "text", "content": { "_width": "half", "label": "EĞİTİM KAPSAMI", "title": "Ne Öğreneceksiniz?", "body": "<ul>...</ul>" }, "order_index": 1 },
{ "id": 3, "type": "text", "content": { "_width": "half", "label": "BAŞVURU ŞARTLARI", "title": "Kimler Katılabilir?", "body": "<ul>...</ul>" }, "order_index": 2 },
{ "id": 4, "type": "stats_grid", "content": { "label": "EĞİTİM SÜRECİ", "title": "Başvurudan Belgeye 4 Adım", "stat_1_value": "01", "stat_1_label": "..." }, "order_index": 3 },
{ "id": 5, "type": "cards", "content": { "label": "KAZANIMLAR", "title": "...", "card_1_title": "...", "card_1_icon": "award" }, "order_index": 4 },
{ "id": 6, "type": "faq", "content": { "title": "...", "faq_1_question": "...", "faq_1_answer": "..." }, "order_index": 5 },
{ "id": 7, "type": "cta", "content": { "title": "...", "button_text": "Ön Kayıt Yap", "button_url": "/kayit?course=..." }, "order_index": 6 }
],
"schedules": [
{ "id": 1, "start_date": "2026-04-07", "end_date": "2026-05-08", "location": "Kadıköy", "quota": 20, "available_seats": 8, "is_urgent": false }
],
"meta_title": "...",
"meta_description": "..."
}
}
```
### Badge Değerleri
| badge | badge_label | Kullanım |
|-------|-------------|----------|
| `most_preferred` | En Çok Tercih Edilen | Kart üstü etiket |
| `popular` | Popüler | Kart üstü etiket |
| `null` | — | Etiket yok |
---
## Eğitim Blokları (Course Blocks)
Bloklar `order_index` sıralı gelir. Her bloğun `type` ve `content` JSON'ı var. **Aynı renderer Page Blocks ile paylaşılabilir.**
### Blok Tipleri
| type | Açıklama | content key'leri |
|------|----------|------------------|
| `text` | Zengin metin | `label`, `title`, `body` (HTML), `_width` |
| `cards` | Kart grid | `label`, `title`, `card_N_title`, `card_N_text`, `card_N_icon` |
| `stats_grid` | İstatistik/adım | `label`, `title`, `stat_N_value`, `stat_N_label`, `style` |
| `cta` | Call-to-action | `title`, `description`, `button_text`, `button_url`, `button_2_text`, `button_2_url` |
| `faq` | SSS | `title`, `faq_N_question`, `faq_N_answer` |
| `hero` | Hero banner | `breadcrumb`, `title`, `highlight`, `subtitle`, `description` |
| `text_image` | Metin + görsel | `label`, `title`, `body`, `image`, `image_alt`, `image_position` |
| `gallery` | Görsel galeri | Görsel listesi |
| `video` | Video embed | Video URL |
| `testimonials` | Referanslar | Yorum kartları |
| `html` | Serbest HTML | Ham HTML |
### `_width` Sistemi
`content._width` blok genişliğini belirler:
- `"full"` (varsayılan, key yoksa otomatik) — tam genişlik
- `"half"` — yarım genişlik, ardışık iki half blok yan yana render edilir
```tsx
// Blok renderer örneği
function BlockRenderer({ blocks }) {
const grouped = groupConsecutiveHalfBlocks(blocks);
return grouped.map((item) => {
if (item.type === 'row') {
return (
<div className="grid grid-cols-2 gap-8">
{item.blocks.map(block => <Block key={block.id} {...block} />)}
</div>
);
}
return <Block key={item.id} {...item} />;
});
}
```
---
## Kategoriler
### GET /v1/categories
```json
{
"data": [
{
"id": 1,
"slug": "guverte",
"label": "Güverte Eğitimleri",
"desc": "...",
"image": null,
"meta_title": "...",
"meta_description": "...",
"courses_count": 10,
"menu_courses": [
{ "title": "ARPA / Radar Simülatör Eğitimi", "slug": "arpa-radar-simulator" },
{ "title": "ECDIS Tip Bazlı Eğitim", "slug": "ecdis-tip-bazli-egitim" },
{ "title": "GMDSS Genel Telsiz Operatörü (GOC)", "slug": "gmdss-genel-telsiz-operatoru-goc" }
]
}
]
}
```
**`menu_courses`**: Her kategoriden `menu_order` 1-3 olan kurslar. Mega menu dropdown'unda kullanılır.
Kategori slug'ları: `guverte`, `stcw`, `makine`, `yat-kaptanligi`, `yenileme`, `guvenlik`
---
## Sayfalar (Pages + Blocks)
### GET /v1/pages/{slug}
Kurumsal sayfalar: `kalite-politikasi`, `hakkimizda`, `vizyon-misyon`
```json
{
"data": {
"id": 1,
"slug": "kalite-politikasi",
"title": "Kalite Politikamız",
"meta_title": "...",
"meta_description": "...",
"is_active": true,
"blocks": [
{ "id": 1, "type": "hero", "content": { "breadcrumb": "...", "title": "...", "highlight": "..." }, "order_index": 0 },
{ "id": 2, "type": "cards", "content": { "label": "AKREDİTASYONLAR", "..." }, "order_index": 1 },
{ "id": 3, "type": "text", "content": { "_width": "half", "..." }, "order_index": 2 },
{ "id": 4, "type": "stats_grid", "content": { "_width": "half", "..." }, "order_index": 3 },
{ "id": 5, "type": "cta", "content": { "..." }, "order_index": 4 }
]
}
}
```
Page blocks ve course blocks **aynı type/content yapısını** kullanır. Tek bir `BlockRenderer` bileşeni her ikisi için de çalışır.
---
## Hero Slides
### GET /v1/hero-slides
Anasayfa slider:
```json
{
"data": [
{
"id": 1,
"title": "Denizcilik Kariyerinize Başlayın",
"subtitle": "STCW uyumlu eğitim programları",
"button_text": "Eğitimleri İncele",
"button_url": "/egitimler",
"image": "/storage/uploads/hero-1.jpg",
"order_index": 0,
"is_active": true
}
]
}
```
---
## Eğitim Takvimi (Schedules)
### GET /v1/schedules
```json
{
"data": [
{
"id": 1,
"course_id": 1,
"course": { "id": 1, "title": "Gemici (Birleşik) Eğitimi", "slug": "gemici-birlesik-egitimi", "..." },
"start_date": "2026-04-07",
"end_date": "2026-05-08",
"location": "Kadıköy",
"quota": 20,
"available_seats": 8,
"is_urgent": false
}
]
}
```
### GET /v1/schedules/upcoming
Yaklaşan eğitimler (start_date >= today, sıralı).
---
## Menüler
### GET /v1/menus/{location}
Location değerleri: `header`, `footer`, `sidebar`
```json
{
"data": [
{
"id": 1,
"label": "Anasayfa",
"url": "/",
"location": "header",
"type": "link",
"parent_id": null,
"order_index": 0,
"is_active": true,
"children": []
}
]
}
```
---
## Duyurular
### GET /v1/announcements
```json
{
"data": [
{
"id": 1,
"slug": "2026-kayitlari-basladi",
"title": "2026 Kayıtları Başladı",
"category": "duyuru",
"excerpt": "...",
"content": "<p>HTML içerik...</p>",
"image": "/storage/uploads/...",
"is_featured": true,
"meta_title": "...",
"meta_description": "...",
"published_at": "2026-03-15"
}
]
}
```
---
## SSS (FAQs)
### GET /v1/faqs
```json
{
"data": [
{
"id": 1,
"question": "STCW belgesi nedir?",
"answer": "...",
"category": "genel",
"order_index": 0,
"is_active": true
}
]
}
```
Kategoriye göre: `GET /v1/faqs/genel`
---
## Lead Formu (Başvuru)
### POST /v1/leads
Rate limited. Request:
```json
{
"name": "Ad Soyad",
"phone": "+90 532 ...",
"source": "web",
"target_course": "gemici-birlesik-egitimi",
"education_level": "lise",
"subject": "Eğitim başvurusu",
"message": "Bilgi almak istiyorum",
"kvkk_consent": true,
"marketing_consent": false,
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "denizcilik-2026"
}
```
---
## Sayfa Önizleme (Preview)
### GET /v1/preview/{token}
Admin panelden gönderilen önizleme. Auth yok, Next.js SSR ile fetch edilir.
10 dakika geçerli. Bulunamazsa 404.
Response formatı `GET /v1/pages/{slug}` ile aynı:
```json
{
"data": {
"id": 1,
"slug": "kalite-politikasi",
"title": "...",
"blocks": [...]
}
}
```
---
## Sayfa Bazlı Veri Haritası
### Anasayfa (`/`)
| Veri | Endpoint |
|------|----------|
| Hero slider | `GET /v1/hero-slides` |
| Eğitim kartları | `GET /v1/courses?per_page=6&sort=-rating` |
| Kategoriler | `GET /v1/categories` |
| Yaklaşan eğitimler | `GET /v1/schedules/upcoming` |
| SSS | `GET /v1/faqs` |
| Site ayarları | `GET /v1/settings` (layout'tan) |
### Eğitimler (`/egitimler`)
| Veri | Endpoint |
|------|----------|
| Eğitim listesi | `GET /v1/courses?category=guverte&per_page=12` |
| Kategoriler (filter) | `GET /v1/categories` |
### Eğitim Detay (`/egitimler/{slug}`)
| Veri | Endpoint |
|------|----------|
| Eğitim + bloklar + takvim | `GET /v1/courses/{slug}` |
### Kurumsal Sayfalar (`/kurumsal/{slug}`)
| Veri | Endpoint |
|------|----------|
| Sayfa + bloklar | `GET /v1/pages/{slug}` |
### Duyurular (`/duyurular`)
| Veri | Endpoint |
|------|----------|
| Duyuru listesi | `GET /v1/announcements` |
### İletişim (`/iletisim`)
| Veri | Endpoint |
|------|----------|
| İletişim bilgileri | `GET /v1/settings/contact` |
| Harita | `GET /v1/settings/maps` |
| Form gönderimi | `POST /v1/leads` |
### Layout (Her Sayfa)
| Veri | Endpoint |
|------|----------|
| Header/Footer/SEO | `GET /v1/settings` |
| Menü | `GET /v1/menus/header` + `GET /v1/menus/footer` |
| Kategoriler (mega menu) | `GET /v1/categories` |
---
## TypeScript Tipleri
```ts
interface Course {
id: number;
category_id: number;
category?: Category;
slug: string;
title: string;
sub: string | null;
desc: string;
long_desc: string;
duration: string;
students: number;
rating: number;
badge: 'most_preferred' | 'popular' | null;
badge_label: string | null;
image: string | null;
price: string;
includes: string[];
requirements: string[];
scope: string[];
standard: string | null;
language: string | null;
location: string | null;
blocks?: Block[];
schedules?: Schedule[];
meta_title: string | null;
meta_description: string | null;
created_at: string;
updated_at: string;
}
interface Category {
id: number;
slug: string;
label: string;
desc: string | null;
image: string | null;
courses_count?: number;
menu_courses?: { title: string; slug: string }[];
}
interface Block {
id: number;
type: 'hero' | 'text' | 'text_image' | 'cards' | 'stats_grid' | 'cta' | 'faq' | 'gallery' | 'video' | 'testimonials' | 'html';
content: Record<string, any>;
order_index: number;
}
interface Schedule {
id: number;
course_id: number;
course?: Course;
start_date: string;
end_date: string;
location: string;
quota: number;
available_seats: number;
is_urgent: boolean;
}
interface Page {
id: number;
slug: string;
title: string;
meta_title: string | null;
meta_description: string | null;
is_active: boolean;
blocks: Block[];
}
interface HeroSlide {
id: number;
title: string;
subtitle: string | null;
button_text: string | null;
button_url: string | null;
image: string | null;
order_index: number;
}
interface Announcement {
id: number;
slug: string;
title: string;
category: string;
excerpt: string | null;
content: string;
image: string | null;
is_featured: boolean;
published_at: string;
}
interface FAQ {
id: number;
question: string;
answer: string;
category: string;
order_index: number;
}
interface Lead {
name: string;
phone: string;
source?: string;
target_course?: string;
education_level?: string;
subject?: string;
message?: string;
kvkk_consent: boolean;
marketing_consent?: boolean;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
}
interface MenuItem {
id: number;
label: string;
url: string;
location: string;
type: string;
parent_id: number | null;
order_index: number;
children: MenuItem[];
}
```
---
## Image URL'leri
Image alanları relative path döner. API base URL ile birleştir:
```ts
const getImageUrl = (path: string | null): string | null => {
if (!path) return null;
if (path.startsWith('http')) return path;
return `${process.env.NEXT_PUBLIC_API_URL}${path}`;
};
```
---
## Cache Stratejisi
| Veri | ISR revalidate |
|------|---------------|
| Settings | 300s (5 dk) |
| Categories | 300s |
| Courses list | 60s |
| Course detail | 60s |
| Pages | 300s |
| Hero slides | 300s |
| Schedules | 60s |
| Announcements | 120s |
| FAQs | 300s |
| Menus | 300s |

293
prompts/frontend-leads.md Normal file
View File

@@ -0,0 +1,293 @@
# Frontend (Next.js) — Lead Form Entegrasyonu
## API Endpoint
```
POST /api/v1/leads
```
Auth yok, public endpoint. Rate limited.
---
## Request Body
```json
{
"name": "Ad Soyad",
"phone": "+90 532 724 15 32",
"email": "ornek@email.com",
"source": "kurs_kayit",
"target_course": "gemici-birlesik-egitimi",
"education_level": "lise",
"subject": "Eğitim başvurusu",
"message": "Bilgi almak istiyorum",
"kvkk_consent": true,
"marketing_consent": false,
"utm_source": "google",
"utm_medium": "cpc",
"utm_campaign": "denizcilik-2026"
}
```
### Zorunlu Alanlar
| Alan | Tip | Açıklama |
|------|-----|----------|
| `name` | string | Ad Soyad (max 255) |
| `phone` | string | Telefon (max 20) |
| `source` | string | Form kaynağı (aşağıya bak) |
| `kvkk_consent` | boolean | **Zorunlu, `true` olmalı** — checkbox onaylatılmalı |
### Opsiyonel Alanlar
| Alan | Tip | Açıklama |
|------|-----|----------|
| `email` | string | E-posta |
| `target_course` | string | Hedef eğitim slug'ı |
| `education_level` | string | Eğitim seviyesi |
| `subject` | string | Konu |
| `message` | string | Mesaj |
| `marketing_consent` | boolean | Pazarlama onayı |
| `utm_source` | string | Google Analytics UTM |
| `utm_medium` | string | — |
| `utm_campaign` | string | — |
---
## Source Değerleri (Form Bazlı)
Her form kendi `source` değerini gönderir:
| Form | source | Zorunlu Alanlar | Opsiyonel Alanlar |
|------|--------|-----------------|-------------------|
| Kurs ön kayıt | `kurs_kayit` | name, phone, kvkk_consent | email, target_course, education_level, message |
| Danışmanlık | `danismanlik` | name, phone, kvkk_consent | email, message, target_course |
| Duyuru sidebar | `duyuru` | name, phone, kvkk_consent | email |
| İletişim | `iletisim` | name, phone, kvkk_consent | email, subject, message |
| Hero form | `hero_form` | name, phone, kvkk_consent | target_course |
| WhatsApp widget | `whatsapp_widget` | name, phone, kvkk_consent | — |
---
## Response
### Başarılı (201):
```json
{
"success": true,
"message": "Talebiniz alınmıştır. En kısa sürede sizinle iletişime geçeceğiz."
}
```
### Validasyon Hatası (422):
```json
{
"message": "KVKK metnini onaylamanız gerekmektedir.",
"errors": {
"kvkk_consent": ["KVKK metnini onaylamanız gerekmektedir."],
"name": ["Ad Soyad zorunludur."]
}
}
```
---
## Form Örnekleri
### 1. Kurs Ön Kayıt Formu (Eğitim Detay Sayfası)
```tsx
async function submitKursKayit(formData: FormData, courseSlug: string) {
const utm = getUTMParams(); // URL'den utm_source, utm_medium, utm_campaign
const res = await fetch(`${API_URL}/api/v1/leads`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: formData.get('name'),
phone: formData.get('phone'),
email: formData.get('email') || undefined,
source: 'kurs_kayit',
target_course: courseSlug,
education_level: formData.get('education_level') || undefined,
message: formData.get('message') || undefined,
kvkk_consent: formData.get('kvkk') === 'on',
marketing_consent: formData.get('marketing') === 'on',
...utm,
}),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.message);
}
return res.json();
}
```
Form alanları:
- Ad Soyad (zorunlu)
- Telefon (zorunlu)
- E-posta
- Eğitim Seviyesi (select: ilkokul, ortaokul, lise, onlisans, lisans)
- Mesaj
- [x] KVKK metnini okudum ve onaylıyorum (zorunlu checkbox)
- [ ] Kampanya ve duyurulardan haberdar olmak istiyorum
### 2. Danışmanlık Formu
```tsx
body: JSON.stringify({
name: formData.get('name'),
phone: formData.get('phone'),
email: formData.get('email') || undefined,
source: 'danismanlik',
target_course: formData.get('interested_course') || undefined,
message: formData.get('message') || undefined,
kvkk_consent: true,
...utm,
})
```
### 3. İletişim Formu
```tsx
body: JSON.stringify({
name: formData.get('name'),
phone: formData.get('phone'),
email: formData.get('email') || undefined,
source: 'iletisim',
subject: formData.get('subject') || undefined,
message: formData.get('message'),
kvkk_consent: true,
...utm,
})
```
### 4. Duyuru Sidebar Mini Form
```tsx
body: JSON.stringify({
name: formData.get('name'),
phone: formData.get('phone'),
source: 'duyuru',
kvkk_consent: true,
...utm,
})
```
### 5. Hero Form (Anasayfa)
```tsx
body: JSON.stringify({
name: formData.get('name'),
phone: formData.get('phone'),
source: 'hero_form',
target_course: formData.get('course') || undefined,
kvkk_consent: true,
...utm,
})
```
---
## UTM Parametreleri
URL'deki UTM parametrelerini otomatik olarak form submit'e ekle:
```tsx
function getUTMParams(): Record<string, string> {
if (typeof window === 'undefined') return {};
const params = new URLSearchParams(window.location.search);
const utm: Record<string, string> = {};
for (const key of ['utm_source', 'utm_medium', 'utm_campaign']) {
const val = params.get(key);
if (val) utm[key] = val;
}
return utm;
}
```
UTM değerleri sayfa boyunca korunmalı (cookie veya sessionStorage):
```tsx
// Sayfa ilk yüklendiğinde
useEffect(() => {
const utm = getUTMParams();
if (Object.keys(utm).length > 0) {
sessionStorage.setItem('utm', JSON.stringify(utm));
}
}, []);
// Form submit'te
const utm = JSON.parse(sessionStorage.getItem('utm') || '{}');
```
---
## KVKK Checkbox
Her formda KVKK onay checkbox'ı **zorunlu**. Backend `kvkk_consent: true` olmadan kabul etmez.
```tsx
<label className="flex items-start gap-2">
<input type="checkbox" name="kvkk" required />
<span className="text-sm text-gray-600">
<a href="/kvkk" target="_blank" className="underline">
KVKK Aydınlatma Metni
</a>'ni okudum ve onaylıyorum.
</span>
</label>
```
---
## Error Handling
```tsx
try {
const result = await submitLead(formData);
// Başarılı — toast göster
toast.success(result.message);
form.reset();
} catch (error) {
if (error.status === 422) {
// Validasyon hataları — form alanlarının altında göster
const { errors } = await error.json();
setFieldErrors(errors);
} else if (error.status === 429) {
// Rate limit — çok fazla istek
toast.error('Çok fazla istek gönderildi. Lütfen biraz bekleyin.');
} else {
toast.error('Bir hata oluştu. Lütfen tekrar deneyin.');
}
}
```
---
## TypeScript Tipi
```ts
interface LeadFormData {
name: string;
phone: string;
email?: string;
source: 'kurs_kayit' | 'danismanlik' | 'duyuru' | 'iletisim' | 'hero_form' | 'whatsapp_widget';
target_course?: string;
education_level?: string;
subject?: string;
message?: string;
kvkk_consent: true; // Her zaman true olmalı
marketing_consent?: boolean;
utm_source?: string;
utm_medium?: string;
utm_campaign?: string;
}
interface LeadResponse {
success: boolean;
message: string;
}
```

View File

@@ -0,0 +1,380 @@
# Frontend (Next.js) — Site Ayarları Entegrasyonu
## Genel Bakış
Backend'den tüm site ayarları **tek bir API çağrısı** ile alınır. Ayarlar gruplara ayrılmış nested JSON formatında gelir. Bu veri layout, header, footer, SEO meta tag'leri, analytics scriptleri ve tüm sabit içerikleri besler.
**Sensitive key'ler (API key, secret, password) public endpoint'ten filtrelenmiştir — güvenle kullanabilirsin.**
---
## API Endpoints (Public — Auth Yok)
| Method | Endpoint | Açıklama |
|--------|----------|----------|
| `GET` | `/api/v1/settings` | Tüm public ayarlar (grouped nested JSON) |
| `GET` | `/api/v1/settings/{group}` | Tek grup ayarları (flat key-value) |
---
## GET /api/v1/settings — Response Formatı
```json
{
"general": {
"site_name": "Boğaziçi Denizcilik Eğitim Kurumu",
"site_tagline": "Türkiye'nin Köklü Denizcilik Okulu",
"site_description": "Türkiye'nin köklü denizcilik eğitim kurumu",
"logo_light": "/storage/uploads/logo-white.png",
"logo_dark": "/storage/uploads/logo-dark.png",
"favicon": "/storage/uploads/favicon.png",
"apple_touch_icon": null,
"announcement_bar_active": "true",
"announcement_bar_text": "2026 Kayıtları Devam Ediyor",
"announcement_bar_url": "/kayit",
"announcement_bar_bg_color": "#1a3e74",
"maintenance_mode": "false",
"maintenance_message": "Sitemiz bakımdadır, kısa süre içinde geri döneceğiz."
},
"contact": {
"phone_primary": "+90 532 724 15 32",
"phone_secondary": null,
"email_info": "bilgi@bogazicidenizcilik.com",
"address_full": "Osmanağa Mah. Çuhadarağa Sk. No:21 Kadıköy/İstanbul",
"address_short": "Kadıköy, İstanbul",
"working_hours_weekday": "Hafta İçi 09:00 17:00",
"whatsapp_number": null,
"whatsapp_message": "Merhaba, bilgi almak istiyorum."
},
"maps": {
"google_maps_embed_url": null,
"google_maps_place_url": null,
"latitude": "40.9876",
"longitude": "29.0234",
"map_zoom_level": "15"
},
"social": {
"instagram_url": null,
"instagram_handle": null,
"facebook_url": null,
"youtube_url": null,
"linkedin_url": null,
"tiktok_url": null,
"twitter_url": null
},
"seo": {
"meta_title_suffix": "| Boğaziçi Denizcilik",
"meta_title_separator": "|",
"default_meta_description": "IMO ve STCW standartlarında denizcilik eğitimi.",
"default_meta_keywords": "denizcilik kursu, STCW eğitimi, kaptan kursu",
"robots": "index, follow",
"canonical_domain": "https://bogazicidenizcilik.com",
"og_image": null,
"og_type": "website",
"og_locale": "tr_TR",
"og_site_name": "Boğaziçi Denizcilik",
"twitter_card_type": "summary_large_image",
"google_site_verification": null
},
"analytics": {
"google_analytics_id": null,
"google_tag_manager_id": null,
"facebook_pixel_id": null,
"hotjar_id": null,
"clarity_id": null,
"crisp_website_id": null,
"custom_head_scripts": null,
"custom_body_scripts": null
},
"header": {
"navbar_style_default": "transparent",
"cta_button_text": "Başvuru Yap",
"cta_button_url": "/kayit",
"cta_button_color": "#396cab",
"show_phone_topbar": "true",
"show_email_topbar": "true",
"show_address_topbar": "true",
"show_hours_topbar": "true",
"show_social_navbar": "true"
},
"footer": {
"footer_description": "Türkiye'nin köklü denizcilik eğitim kurumlarından biri olarak...",
"footer_logo": null,
"copyright_text": "© 2026 Boğaziçi Denizcilik Eğitim Kurumu. Tüm hakları saklıdır.",
"footer_address": "Osmanağa Bahariye Cad. No:31 Kadıköy/İstanbul",
"footer_phone": "+90 532 724 15 32",
"footer_email": "bilgi@bogazicidenizcilik.com",
"footer_bg_color": "#0f2447",
"show_social_footer": "true"
}
}
```
> **Not:** `integrations` grubu public API'den dönmez (tamamı `is_public: false`). `maps.google_maps_api_key` de filtrelenmiştir.
---
## GET /api/v1/settings/{group} — Tek Grup
```
GET /api/v1/settings/contact
```
```json
{
"phone_primary": "+90 532 724 15 32",
"email_info": "bilgi@bogazicidenizcilik.com",
"address_full": "Osmanağa Mah. Çuhadarağa Sk. No:21 Kadıköy/İstanbul",
...
}
```
Geçerli group değerleri: `general`, `contact`, `maps`, `social`, `seo`, `analytics`, `header`, `footer`
---
## Önerilen Veri Akışı (Next.js)
### 1. Layout-Level Fetch (Server Component)
Ayarlar tüm sayfalarda gerekli (header, footer, SEO). Layout'ta bir kez fetch edip context/provider ile dağıt:
```tsx
// app/layout.tsx
async function getSettings() {
const res = await fetch(`${process.env.API_URL}/api/v1/settings`, {
next: { revalidate: 300 } // 5 dk cache
});
return res.json();
}
export default async function RootLayout({ children }) {
const settings = await getSettings();
return (
<html>
<head>
{/* Analytics Scripts */}
{settings.analytics?.google_tag_manager_id && (
<script>...</script>
)}
{settings.analytics?.custom_head_scripts && (
<div dangerouslySetInnerHTML={{ __html: settings.analytics.custom_head_scripts }} />
)}
</head>
<body>
<SettingsProvider value={settings}>
<Header settings={settings} />
{children}
<Footer settings={settings} />
</SettingsProvider>
{settings.analytics?.custom_body_scripts && (
<div dangerouslySetInnerHTML={{ __html: settings.analytics.custom_body_scripts }} />
)}
</body>
</html>
);
}
```
### 2. Boolean Değerler
Backend tüm değerleri **string** olarak döner. Boolean kontrol:
```tsx
// Helper fonksiyon
const isEnabled = (value: string | null) => value === "true";
// Kullanım
{isEnabled(settings.general.announcement_bar_active) && (
<AnnouncementBar
text={settings.general.announcement_bar_text}
url={settings.general.announcement_bar_url}
bgColor={settings.general.announcement_bar_bg_color}
/>
)}
{isEnabled(settings.header.show_phone_topbar) && (
<span>{settings.contact.phone_primary}</span>
)}
```
### 3. Image Alanları
Image değerleri relative path olarak gelir. API base URL ile birleştir:
```tsx
const getImageUrl = (path: string | null) => {
if (!path) return null;
if (path.startsWith('http')) return path;
return `${process.env.NEXT_PUBLIC_API_URL}${path}`;
};
// Kullanım
<Image src={getImageUrl(settings.general.logo_light)} alt={settings.general.site_name} />
```
---
## Bileşen Bazlı Kullanım Haritası
### Header / Navbar
| Ayar | Kullanım |
|------|----------|
| `general.logo_light` | Navbar logo (transparent bg) |
| `general.logo_dark` | Navbar logo (scrolled/white bg) |
| `general.announcement_bar_*` | Üst duyuru barı |
| `header.navbar_style_default` | Navbar başlangıç stili |
| `header.cta_button_text/url/color` | Sağ üst CTA butonu |
| `header.show_*_topbar` | Topbar'da göster/gizle kontrolleri |
| `header.show_social_navbar` | Sosyal medya ikonları |
| `contact.phone_primary` | Topbar telefon |
| `contact.email_info` | Topbar e-posta |
| `contact.address_short` | Topbar adres |
| `contact.working_hours_weekday` | Topbar çalışma saatleri |
| `social.*_url` | Sosyal medya ikon linkleri |
### Footer
| Ayar | Kullanım |
|------|----------|
| `footer.footer_logo` | Footer logo |
| `footer.footer_description` | Footer açıklama metni |
| `footer.footer_address` | Adres |
| `footer.footer_phone` | Telefon |
| `footer.footer_email` | E-posta |
| `footer.copyright_text` | Copyright satırı |
| `footer.footer_bg_color` | Background rengi |
| `footer.show_social_footer` | Sosyal medya göster/gizle |
| `social.*_url` | Sosyal medya ikon linkleri |
### SEO / Head Meta Tags
```tsx
// Her sayfa için generateMetadata
export async function generateMetadata(): Promise<Metadata> {
const settings = await getSettings();
const seo = settings.seo;
return {
title: {
template: `%s ${seo.meta_title_separator} ${seo.og_site_name}`,
default: settings.general.site_name,
},
description: seo.default_meta_description,
keywords: seo.default_meta_keywords,
robots: seo.robots,
openGraph: {
type: seo.og_type,
locale: seo.og_locale,
siteName: seo.og_site_name,
images: seo.og_image ? [getImageUrl(seo.og_image)] : [],
},
twitter: {
card: seo.twitter_card_type,
site: seo.twitter_site,
creator: seo.twitter_creator,
},
verification: {
google: seo.google_site_verification,
other: {
'yandex-verification': seo.yandex_verification,
'msvalidate.01': seo.bing_site_verification,
},
},
};
}
```
### İletişim Sayfası
| Ayar | Kullanım |
|------|----------|
| `contact.phone_primary/secondary` | Telefon numaraları |
| `contact.email_*` | E-posta adresleri |
| `contact.address_full` | Tam adres |
| `contact.working_hours_*` | Çalışma saatleri |
| `contact.whatsapp_number/message` | WhatsApp butonu |
| `maps.google_maps_embed_url` | Harita iframe |
| `maps.google_maps_place_url` | "Google Maps'te Aç" linki |
| `maps.latitude/longitude` | Marker konumu |
### WhatsApp Floating Button
```tsx
const whatsappUrl = settings.contact.whatsapp_number
? `https://wa.me/${settings.contact.whatsapp_number.replace(/\s/g, '')}?text=${encodeURIComponent(settings.contact.whatsapp_message || '')}`
: null;
{whatsappUrl && <a href={whatsappUrl} target="_blank">WhatsApp</a>}
```
### Bakım Modu
```tsx
// middleware.ts veya layout.tsx
if (isEnabled(settings.general.maintenance_mode)) {
return <MaintenancePage message={settings.general.maintenance_message} />;
}
```
---
## Cache Stratejisi
| Strateji | Açıklama |
|----------|----------|
| Backend | 1 saat cache (`Cache::remember`, 3600s) |
| Frontend (ISR) | `next: { revalidate: 300 }` — 5 dakika |
| Admin güncelleme sonrası | Backend cache otomatik temizlenir, frontend 5 dk içinde güncellenir |
| Acil güncelleme | Admin panelden "Cache Temizle" → frontend revalidate |
---
## Null Değer Kontrolü
Birçok ayar başlangıçta `null` olabilir. Her kullanımda null check yap:
```tsx
// Kötü
<a href={settings.social.instagram_url}>Instagram</a>
// İyi
{settings.social?.instagram_url && (
<a href={settings.social.instagram_url}>Instagram</a>
)}
```
Sosyal medya ikonlarını dinamik render et — sadece URL'si dolu olanları göster:
```tsx
const socialLinks = [
{ key: 'instagram_url', icon: Instagram, label: 'Instagram' },
{ key: 'facebook_url', icon: Facebook, label: 'Facebook' },
{ key: 'twitter_url', icon: Twitter, label: 'X' },
{ key: 'youtube_url', icon: Youtube, label: 'YouTube' },
{ key: 'linkedin_url', icon: Linkedin, label: 'LinkedIn' },
{ key: 'tiktok_url', icon: TikTok, label: 'TikTok' },
];
{socialLinks
.filter(s => settings.social?.[s.key])
.map(s => (
<a key={s.key} href={settings.social[s.key]} target="_blank" aria-label={s.label}>
<s.icon />
</a>
))
}
```
---
## TypeScript Tipi (Önerilen)
```ts
interface SiteSettings {
general: Record<string, string | null>;
contact: Record<string, string | null>;
maps: Record<string, string | null>;
social: Record<string, string | null>;
seo: Record<string, string | null>;
analytics: Record<string, string | null>;
header: Record<string, string | null>;
footer: Record<string, string | null>;
}
```