Compare commits
303 Commits
main
..
0d3a514456
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d3a514456 | |||
| 99e97edfd1 | |||
| db60909348 | |||
| 080c39ac88 | |||
| c3f343ad70 | |||
| fb1224f7c9 | |||
| f68bc87031 | |||
| fee4c8fa78 | |||
| e8352f599b | |||
| 80de8dc6f9 | |||
| e4e7599cb2 | |||
| 88c1657b6a | |||
| f195d22342 | |||
| 864f80c7b8 | |||
| d245055bf4 | |||
| 9c4dc0ff11 | |||
| 092eb09316 | |||
| 82ea32cd33 | |||
| 2ee018c37c | |||
| 81690db06d | |||
| a9c872e5b4 | |||
| 97e95efb17 | |||
| 181ec53ef0 | |||
| 0a3feba23a | |||
| f099d8f46a | |||
| 36ec1823c8 | |||
| 23f24e426b | |||
| cf4572cb03 | |||
| efa575a920 | |||
| fc2c99f0c2 | |||
| 3235a700c9 | |||
| e23ac8c346 | |||
| 93e45655eb | |||
| 1b0800beb6 | |||
| d828df82bb | |||
| 690ee11336 | |||
| c719e58d4e | |||
| 7e2cd6eff4 | |||
| 0bdf0c3394 | |||
| d7200111e2 | |||
| faf752dd3c | |||
| 31e695cd3a | |||
| f77866faa5 | |||
| fbbed52a3d | |||
| e7ad0123ac | |||
| f34fc0f2a0 | |||
| 9e6bcc22b6 | |||
| 3c023ba588 | |||
| ddbfc7e3a7 | |||
| 59d9dfe40d | |||
| f6091f1bb8 | |||
| 57d092c0a3 | |||
| 0649d38c48 | |||
| aa48ce7890 | |||
| e24b4bdf41 | |||
| 76081f0e85 | |||
| e0941fe931 | |||
| 1ef535d132 | |||
| b7ef2450bd | |||
| 38b06f9d1c | |||
| 5bc2fd0029 | |||
| d39acfc582 | |||
| 5a1dfb51a2 | |||
| af2b57c6d1 | |||
| dd0bee9355 | |||
| af147f5fea | |||
| 3c92a81cfe | |||
| 05e8059b5d | |||
| 5848cd5189 | |||
| c68e95631b | |||
| 8c4c58e094 | |||
| b7434e12c6 | |||
| 121ba7d9c0 | |||
| e054b04b06 | |||
| fe3d88c467 | |||
| 01a708b054 | |||
| bd1ccb29c0 | |||
| 8a08a0a89a | |||
| 5d88241595 | |||
| 4ec2683317 | |||
| 6fb2bfb265 | |||
| 5f6c468a4b | |||
| 48ae773e85 | |||
| 4e98034536 | |||
| f2b5a29b15 | |||
| 3d2f2ab64e | |||
| 7d7c018496 | |||
| 92f14b2f4f | |||
| bc59a32244 | |||
| 48f03cb83b | |||
| 16f9d5a58a | |||
| a4bc64527c | |||
| 910e912b05 | |||
| ba98e17d2d | |||
| abeed5ae88 | |||
| c28450b97d | |||
| 40fbb750db | |||
| e9ddc3d332 | |||
| 98355a5286 | |||
| 5fc0462ab1 | |||
| 81b94e8635 | |||
| 92af30f61b | |||
| 3cd5ee9e99 | |||
| 8bf36c7d39 | |||
| a9fe4599cd | |||
| f4500a490b | |||
| 547b6efc6c | |||
| c5a33f4d18 | |||
| 15ba98e14d | |||
| 0fe2775420 | |||
| f69e7bae7b | |||
| c1dc4629c0 | |||
| 92f9147e80 | |||
| 809f796f56 | |||
| 3b83f01952 | |||
| 654a7ed175 | |||
| b4a81d703a | |||
| afa92d8f4a | |||
| a037917f16 | |||
| 22c5e40cfc | |||
| 1b920e2b5b | |||
| 8c00956f29 | |||
| 3ed63cacb3 | |||
| b36ccf298b | |||
| 2d6af94c88 | |||
| dd013a471d | |||
| 79ca5d49b6 | |||
| 5bf876d135 | |||
| 6665ed82b4 | |||
| c668435b15 | |||
| 6abec883c5 | |||
| 1da2e3771a | |||
| a94fb17211 | |||
| a61c80ee3d | |||
| 72c1234b5c | |||
| 2b15c17e60 | |||
| 45c9fa24d0 | |||
| c2767f031a | |||
| a484b19378 | |||
| 7180d44f6d | |||
| 0c51fecef8 | |||
| 945c3ca2ec | |||
| 945e250f17 | |||
| fed91996fc | |||
| d1c2ccd985 | |||
| 3fb2106707 | |||
| 7ae1dcfab8 | |||
| b2e6f89bcb | |||
| 53a265844f | |||
| 607763ff52 | |||
| 752876fed9 | |||
| 08c1e28427 | |||
| 7e3700a2c2 | |||
| 61cc5c8dda | |||
| 11cb89c944 | |||
| ead0077fde | |||
| f0ab100b07 | |||
| c195ff79d2 | |||
| 474882ba7c | |||
| ddf9640222 | |||
| 6d9ffc808b | |||
| 8675325f50 | |||
| b678e7f833 | |||
| ab08b6b840 | |||
| d97a9ed69e | |||
| f75632d0e8 | |||
| d24c9e4502 | |||
| 506a98163a | |||
| 2018860c23 | |||
| cc6ca1f660 | |||
| d6bfc44646 | |||
| 42c3b3ac45 | |||
| 69710a71b9 | |||
| 4150931ac8 | |||
| c82dd5c6b6 | |||
| a9d6f17e3c | |||
| 502460be79 | |||
| cda0bb49bc | |||
| 55efbc6614 | |||
| 62540c4a98 | |||
| a79d00a8dd | |||
| b51cf5876f | |||
| 31a79ca9dd | |||
| 0be000d474 | |||
| 6a4a0101a5 | |||
| b487613b71 | |||
| a132da44f9 | |||
| da0ee2477b | |||
| 1eab2c856b | |||
| 1b00a3120e | |||
| af36454b8f | |||
| fd30e5d1b7 | |||
| dbb9853066 | |||
| 0ca112b28d | |||
| 34f790cc10 | |||
| 15896248c4 | |||
| 33c7b6a51e | |||
| 276e4b30ca | |||
| 187f8c3812 | |||
| ad4f04b005 | |||
| e18c8bbbcf | |||
| ae3a697170 | |||
| e9f737ab1d | |||
| 751b9339f0 | |||
| f35632ee0d | |||
| e232d25fc8 | |||
| bc65bb10ef | |||
| 3c5c4c5e2a | |||
| dbadfee9ed | |||
| 51ae8fdd9c | |||
| 4168e1c8d4 | |||
| 575fdad12c | |||
| 19c9202fc1 | |||
| 29ddc24602 | |||
| fc0ebf1a28 | |||
| bbe26d22af | |||
| 1950e8734c | |||
| 3512858304 | |||
| ebcbc9842c | |||
| 81790642c3 | |||
| 7cbf872b05 | |||
| 6a7e977330 | |||
| 5f26e76143 | |||
| 932bb89454 | |||
| 2fd3dbb63f | |||
| 9a606991e3 | |||
| a81f4fdf9e | |||
| 1547d07cb3 | |||
| c5f2b7fc43 | |||
| 7af164f3d7 | |||
| 8f7c5e2102 | |||
| 45440f2814 | |||
| 4924cb0dc8 | |||
| f4f601a492 | |||
| 42357c4dbb | |||
| 452d05f4a1 | |||
| 477993ec83 | |||
| 2daa0f5ddd | |||
| 7101f7878a | |||
| a5847dcd58 | |||
| 0fa8e4e435 | |||
| 8f85afd316 | |||
| bda19b4b0f | |||
| 7c65a4a592 | |||
| b289cfd829 | |||
| 177ebff119 | |||
| 33c1ffe779 | |||
| e4ca3b228b | |||
| 2f9fb14bac | |||
| 43bcca4a5e | |||
| dbd267ca17 | |||
| 74779ef2c8 | |||
| 4a73f7a3a0 | |||
| 03ff01f24d | |||
| 90565412a4 | |||
| dc63563e7f | |||
| 1400ec1ca7 | |||
| 7946876434 | |||
| ca942c5140 | |||
| e80c94146d | |||
| 2fdf187658 | |||
| 3ad69677d8 | |||
| 6caa15b501 | |||
| 171132263b | |||
| 9e79ec28d9 | |||
| 3b852512f0 | |||
| b00ca5ea78 | |||
| c73d448f47 | |||
| 374d4a8d7d | |||
| 3a2661cf95 | |||
| 36471b5965 | |||
| 194e575619 | |||
| a1d02de86b | |||
| 1fc23e7bda | |||
| 97092dd793 | |||
| 89eb9f5c4e | |||
| 0ca75829bb | |||
| e8cdba4fd3 | |||
| 12441517f6 | |||
| 10f2e9c9bb | |||
| 5aa2a3d6e5 | |||
| 3e4fc26e4b | |||
| 68b7907981 | |||
| d75434674f | |||
| cb0bbf87c7 | |||
| 77a2713aec | |||
| 1fa194260e | |||
| 009d1cbb76 | |||
| 2a1cdafbea | |||
| b345c6f71c | |||
| a2827c5d7a | |||
| 194beaee1c | |||
| cf855fdd70 | |||
| 6b561896c6 | |||
| 922add1b6f | |||
| 2c9dcac76c | |||
| 064ec327ba | |||
| f381caaa39 | |||
| 54944317b3 | |||
| f4cf16f93f | |||
| a85e6eebe0 | |||
| a5a3248c69 | |||
| 21ebbfc749 |
@@ -0,0 +1,92 @@
|
||||
---
|
||||
description: Frontend blok ve section mimarisi kuralları. Yeni blok eklerken veya sayfa yapısı kurarken bu kuralları uygula.
|
||||
globs: resources/views/components/blocks/**/*.blade.php, app/Filament/**/*.php, resources/views/templates/**/*.blade.php
|
||||
---
|
||||
|
||||
# Frontend Architecture Rules
|
||||
|
||||
Bu kurallar, projenin Blok (Block) ve Bölüm (Section) tabanlı mimarisini tanımlar. Sayfa oluştururken bu standartlara uyulmalıdır.
|
||||
|
||||
## 1. Blok (Block) Yapısı
|
||||
|
||||
Sistem, `resources/views/components/blocks/` altında bulunan hazır Blade bileşenleri üzerine kuruludur.
|
||||
|
||||
### Mevcut Bloklar ve Kullanımları
|
||||
- **Hero:** Ana başlık, büyük görsel ve CTA butonları.
|
||||
- **Features:** İkonlu özellik kutuları grid yapısı.
|
||||
- **Stats:** Sayısal istatistikler ve sayaçlar.
|
||||
- **Testimonials:** Müşteri yorumları ve referanslar.
|
||||
- **Pricing:** Fiyatlandırma tabloları.
|
||||
- **CTA:** Aksiyon çağrısı bantları.
|
||||
- **Gallery:** Proje veya görsel galerisi.
|
||||
- **Team:** Ekip üyeleri listesi.
|
||||
- **Contact:** İletişim bilgileri ve formu.
|
||||
|
||||
### Blok Geliştirme Kuralları
|
||||
1. **Component Yapısı:** Her blok `resources/views/components/blocks/{type}.blade.php` konumunda olmalıdır.
|
||||
2. **Veri Alımı:** Bloklar veriyi sadece `$data` prop'u üzerinden almalıdır:
|
||||
```blade
|
||||
@props(['data' => []])
|
||||
{{-- Erişim: $data['title'] --}}
|
||||
```
|
||||
3. **Null Check:** Tüm veri alanları için `isset` veya `??` kontrolü yapılmalıdır.
|
||||
```blade
|
||||
{{ $data['title'] ?? '' }}
|
||||
@if(isset($data['image'])) ... @endif
|
||||
```
|
||||
|
||||
## 2. Section Builder ve Veri Yapısı
|
||||
|
||||
Admin panelinde "Section Builder" kullanılarak veriler JSON formatında tutulur.
|
||||
|
||||
### Veri Formatı (JSON)
|
||||
Veriler `sections` sütununda veya `parsed_sections` attribute'unda şu formatta işlenir:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"type": "hero",
|
||||
"data": {
|
||||
"title": "Başlık",
|
||||
"subtitle": "Alt Başlık",
|
||||
"background_image": "assets/img/bg.jpg"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Key İsimlendirme Kuralları (ÖNEMLİ)
|
||||
Veri anahtarları (keys) her zaman **İngilizce** ve **snake_case** olmalıdır:
|
||||
- ✅ `title`, `sub_title`, `button_text`, `image_url`
|
||||
- ❌ `baslik`, `altBaslik`, `butonYazisi`, `resim`
|
||||
|
||||
### Önerilen Value Tipleri
|
||||
Admin panelinde alan tanımlarken doğru tipleri seçin:
|
||||
- **Kısa Metinler:** `Text` (title, badge)
|
||||
- **Uzun Metinler:** `Textarea` (description)
|
||||
- **Formatlı İçerik:** `Rich Text` veya `HTML` (content)
|
||||
- **Görseller:** `Image` (background_image, icon)
|
||||
- **Linkler:** `URL` (button_url)
|
||||
|
||||
## 3. Blade Entegrasyonu
|
||||
|
||||
Sayfa şablonlarında (örneğin `home.blade.php`) blokları render ederken dinamik yapı kullanılmalıdır:
|
||||
|
||||
```blade
|
||||
@foreach($page->parsed_sections as $section)
|
||||
@if(view()->exists("components.blocks.{$section['type']}"))
|
||||
<x-dynamic-component
|
||||
:component="'blocks.'.$section['type']"
|
||||
:data="$section['data']"
|
||||
/>
|
||||
@endif
|
||||
@endforeach
|
||||
```
|
||||
|
||||
## 4. Stil Özelleştirme
|
||||
Bloklar, dışarıdan gelen stil parametrelerini desteklemelidir:
|
||||
- `bg_class`: Arka plan rengi (örn: `!bg-[#f0f0f8]`)
|
||||
- `column_class`: Grid yapısı (örn: `md:w-4/12`)
|
||||
- `padding_class`: Boşluklar (örn: `py-14`)
|
||||
|
||||
Bu sınıflar Blade içinde ana kapsayıcıya (wrapper) eklenmelidir.
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
description: Public HTML şablonlarını Laravel Blade yapısına dönüştürme kuralları
|
||||
globs: resources/views/**/*.blade.php, public/html/**/*.html
|
||||
---
|
||||
# Frontend Theme Implementation Rules
|
||||
|
||||
Bu kurallar, `public/html` dizinindeki statik HTML şablonlarının Laravel Blade yapısına dönüştürülmesi sırasında geçerlidir.
|
||||
|
||||
## 1. Kaynak ve Hedef Yapısı
|
||||
|
||||
- **Kaynak (Source):** `public/html/` dizinindeki `.html` dosyaları referans alınır.
|
||||
- **Hedef (Target):** `resources/views/front/` dizini altında Blade dosyaları oluşturulur.
|
||||
- **Assets (Varlıklar):** Statik dosyalar `public/html/assets` altındadır. Blade içinde erişim `asset('html/assets/...')` şeklinde olmalıdır.
|
||||
|
||||
## 2. Bileşen (Component) Yapısı
|
||||
|
||||
Yeni UI bileşenleri oluştururken aşağıdaki isimlendirme ve klasör yapısını kullan:
|
||||
|
||||
- **Layout:** `resources/views/layouts/front.blade.php` (Master layout)
|
||||
- **Header:** `resources/views/components/front/header.blade.php` -> `<x-front.header />`
|
||||
- **Footer:** `resources/views/components/front/footer.blade.php` -> `<x-front.footer />`
|
||||
- **Page Sections:** `resources/views/components/front/sections/` altında:
|
||||
- Hero/Slider -> `<x-front.sections.hero />`
|
||||
- Features -> `<x-front.sections.features />`
|
||||
- About -> `<x-front.sections.about />`
|
||||
|
||||
## 3. Kod Dönüştürme Kuralları
|
||||
|
||||
### A. CSS ve Sınıflar
|
||||
- **ÖNEMLİ:** Şablondaki `!` önekli Tailwind sınıflarını (örn: `!bg-[#f0f0f8]`, `!absolute`) KESİNLİKLE KORU. Bunlar temanın çalışması için kritiktir.
|
||||
- HTML'deki `class="..."` yapılarını birebir kopyala, Tailwind sınıflarını sadeleştirme veya değiştirme.
|
||||
|
||||
### B. Asset Bağlantıları
|
||||
HTML içindeki:
|
||||
```html
|
||||
<link rel="stylesheet" href="assets/css/plugins.css">
|
||||
<img src="assets/img/logo.png">
|
||||
```
|
||||
Blade içinde şu şekilde olmalı:
|
||||
```blade
|
||||
<link rel="stylesheet" href="{{ asset('html/assets/css/plugins.css') }}">
|
||||
<img src="{{ asset('html/assets/img/logo.png') }}">
|
||||
```
|
||||
|
||||
### C. Dinamik İçerik
|
||||
- Metinleri `{{ __('theme.key') }}` şeklinde lokalizasyon fonksiyonu ile sar.
|
||||
- Tekrarlayan elemanları (örn: slider itemları, hizmet kutuları) `@foreach` döngüsü içine al.
|
||||
|
||||
## 4. Layout Şablonu (Örnek)
|
||||
|
||||
`front.blade.php` layout dosyası şu yapıda olmalıdır:
|
||||
|
||||
```blade
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>@yield('title', config('app.name'))</title>
|
||||
<!-- CSS Assets from public/html/index.html head -->
|
||||
@stack('styles')
|
||||
</head>
|
||||
<body class="!bg-[#f0f0f8]">
|
||||
|
||||
<x-front.header />
|
||||
|
||||
<main class="grow shrink-0">
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
<x-front.footer />
|
||||
|
||||
<!-- JS Assets from public/html/index.html bottom -->
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 5. CMS Entegrasyonu (Dinamik Şablon Seçimi)
|
||||
|
||||
Kullanıcı, bir prompt ile belirli bir section, header veya footer şablonunun kullanılmasını istediğinde (örn: "Header V1 ve About Section 3 kullan"), aşağıdaki stratejiyi izle:
|
||||
|
||||
1. **Veritabanı Araması:** İlgili şablonun veritabanında (genellikle `sections` veya `templates` tablosunda) var olduğunu varsay.
|
||||
2. **Seeder/Migration Oluşturma:** Kullanıcının istediği yapıyı oluşturmak için bir Laravel Seeder veya Migration kodu üret. Bu kod:
|
||||
- İlgili şablonu (title veya slug ile) bulmalı.
|
||||
- Sayfa veya layout ile ilişkilendirmeli.
|
||||
|
||||
**Örnek Senaryo:** "Ana sayfada 'Hero Slider V2' ve 'Services Grid' kullan."
|
||||
**Üretilecek Kod (Örnek):**
|
||||
```php
|
||||
// DatabaseSeeder.php veya özel bir seeder içinde
|
||||
$homePage = Page::firstOrCreate(['slug' => 'home'], ['title' => 'Ana Sayfa']);
|
||||
|
||||
// Şablonları bul
|
||||
$heroSection = Section::where('title', 'like', '%Hero Slider V2%')->first();
|
||||
$servicesSection = Section::where('title', 'like', '%Services Grid%')->first();
|
||||
|
||||
// İlişkilendir (Proje yapısına göre attach veya update)
|
||||
if ($heroSection) {
|
||||
// Örnek ilişkilendirme mantığı
|
||||
$homePage->sections()->attach($heroSection->id, ['order' => 1]);
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Otomatik Menü ve Sayfa Yapılandırması
|
||||
|
||||
Kullanıcı "Şu yapıda bir menü tasarla" dediğinde (örn: "Ana Sayfa, Kurumsal > Hakkımızda, İletişim"):
|
||||
|
||||
1. **Page Modeli:** İstenen sayfaların veritabanında var olup olmadığını kontrol eden ve yoksa oluşturan bir kod bloğu hazırla.
|
||||
2. **Menu Modeli:** Sayfaları hiyerarşik olarak menü yapısına ekleyen bir kod bloğu hazırla.
|
||||
3. **Çıktı:** Bu işlemleri gerçekleştirecek bir `php artisan tinker` komutu veya `MenuSeeder` sınıfı sun.
|
||||
|
||||
**Örnek Prompt:** "Menü: Ana Sayfa, Kurumsal (Hakkımızda, Vizyon), İletişim"
|
||||
**Beklenen Aksiyon:**
|
||||
```php
|
||||
// 1. Sayfaları Oluştur
|
||||
$pages = [
|
||||
'Ana Sayfa' => '/',
|
||||
'Kurumsal' => '#',
|
||||
'Hakkımızda' => '/kurumsal/hakkimizda',
|
||||
'Vizyon' => '/kurumsal/vizyon',
|
||||
'İletişim' => '/iletisim'
|
||||
];
|
||||
|
||||
foreach ($pages as $title => $url) {
|
||||
Page::firstOrCreate(['title' => $title], ['slug' => Str::slug($title)]);
|
||||
}
|
||||
|
||||
// 2. Menü Yapısını Kur
|
||||
$mainMenu = Menu::firstOrCreate(['name' => 'Main Menu']);
|
||||
// ... Menü elemanlarını ekleyen kod ...
|
||||
```
|
||||
|
||||
## 7. Analiz Stratejisi
|
||||
Bir sayfayı dönüştürürken:
|
||||
1. Önce `public/html` altındaki ilgili `.html` dosyasını oku.
|
||||
2. `header` ve `footer` kısımlarını ayır.
|
||||
3. `main` içeriğindeki her bir `section` veya mantıksal bloğu ayrı bir Blade component olarak çıkar.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
description: Google yapılandırılmış veri (JSON-LD) standartları
|
||||
globs: app/Support/*StructuredData*.php, app/Http/Controllers/**/*.php, resources/views/**/*.blade.php
|
||||
---
|
||||
# Yapılandırılmış Veri (Structured Data) Kuralları
|
||||
|
||||
Google [Yapılandırılmış Veri Genel Yönergeleri](https://developers.google.com/search/docs/appearance/structured-data/sd-policies?hl=tr) ve [Giriş](https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data?hl=tr) dokümanlarına uy.
|
||||
|
||||
## Temel İlkeler
|
||||
|
||||
1. **JSON-LD kullan** — Google'ın önerdiği format; `<head>` içinde `<script type="application/ld+json">` ile sunulur.
|
||||
2. **Sayfa içeriğiyle eşleş** — İşaretlenen veri kullanıcının gördüğü içerikle birebir uyumlu olmalı; görünmeyen veya yanlış bilgi ekleme.
|
||||
3. **Eksiksiz ve doğru özellikler** — Zorunlu alanları doldur; eksik/hatalı önerilen alanları doldurmaya çalışmak yerine az ama doğru veri tercih et.
|
||||
4. **schema.org sözlüğü** — `@context: https://schema.org` kullan; Google Arama özellikleri için [Search Central özellik rehberlerini](https://developers.google.com/search/docs/appearance/structured-data/search-gallery?hl=tr) referans al.
|
||||
|
||||
## Proje Mimarisi
|
||||
|
||||
```
|
||||
app/Support/
|
||||
├── StructuredData.php # Ortak yardımcılar (WebSite, Organization, WebPage, BreadcrumbList)
|
||||
├── PageStructuredData.php # CMS sayfaları
|
||||
├── BlogStructuredData.php # Blog listesi ve detay
|
||||
└── MusicProductionStructuredData.php
|
||||
```
|
||||
|
||||
### Controller → View akışı
|
||||
|
||||
```php
|
||||
// Controller
|
||||
'structuredData' => BlogStructuredData::forShow($post, url()->current()),
|
||||
|
||||
// Layout (layouts/site.blade.php) otomatik render eder:
|
||||
@if(!empty($structuredData))
|
||||
<x-seo.json-ld :data="$structuredData" />
|
||||
@endif
|
||||
```
|
||||
|
||||
**Asla** Blade içinde ham JSON string interpolasyonu kullanma — `json_encode` ile `<x-seo.json-ld>` bileşenini kullan.
|
||||
|
||||
## Sayfa Türü → Schema Eşlemesi
|
||||
|
||||
| Sayfa türü | Schema türleri | Sınıf |
|
||||
|---|---|---|
|
||||
| Ana sayfa / CMS sayfaları | WebSite, Organization, WebPage, BreadcrumbList | `PageStructuredData` |
|
||||
| Blog listesi | CollectionPage, ItemList, BlogPosting (özet) | `BlogStructuredData::forIndex` |
|
||||
| Blog detay | BlogPosting, WebPage, BreadcrumbList | `BlogStructuredData::forShow` |
|
||||
| Müzik prodüksiyonları | CollectionPage / MusicAlbum, ItemList | `MusicProductionStructuredData` |
|
||||
| Ürün/hizmet sayfaları | WebPage, Product veya Service | Henüz eklenmedi |
|
||||
| Kariyer / staj | WebPage | Henüz eklenmedi |
|
||||
| İletişim | WebPage, Organization (contactPoint) | Henüz eklenmedi |
|
||||
|
||||
## @graph Deseni
|
||||
|
||||
Birden fazla entity için `@graph` kullan; `@id` ile cross-reference yap:
|
||||
|
||||
```php
|
||||
return self::wrap([
|
||||
self::websiteNode(),
|
||||
self::organizationNode(),
|
||||
self::webPageNode($pageUrl, $name, $description),
|
||||
self::breadcrumbNode($pageUrl, $items),
|
||||
// sayfa-özel entity (BlogPosting, MusicAlbum, vb.)
|
||||
]);
|
||||
```
|
||||
|
||||
## Yeni Sayfa Eklerken
|
||||
|
||||
1. `app/Support/` altında `*StructuredData.php` sınıfı oluştur veya mevcut sınıfı genişlet.
|
||||
2. `StructuredData` base sınıfındaki ortak node'ları kullan (DRY).
|
||||
3. Controller'da `structuredData` key'ini view'a geçir.
|
||||
4. Görünür içerikle eşleştiğini doğrula (başlık, açıklama, görsel, tarih).
|
||||
5. [Zengin Sonuçlar Testi](https://search.google.com/test/rich-results) ile doğrula.
|
||||
|
||||
## Yasaklar
|
||||
|
||||
- ❌ Boş veya içeriksiz sayfalara yalnızca schema eklemek
|
||||
- ❌ Blade'de `"headline": "{{ $title }}"` gibi kaçışsız JSON
|
||||
- ❌ `@push('scripts')` ile body sonuna JSON-LD koymak (head'de olmalı)
|
||||
- ❌ data-vocabulary.org işaretlemesi
|
||||
- ❌ Kullanıcıya görünmeyen rating/review verisi uydurmak
|
||||
|
||||
## Doğrulama
|
||||
|
||||
- Geliştirme: [Zengin Sonuçlar Testi](https://search.google.com/test/rich-results)
|
||||
- Prod: Search Console → Zengin sonuçlar raporları
|
||||
@@ -63,3 +63,24 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
# Spotify Web API (Müzik Prodüksiyonları senkronizasyonu)
|
||||
# NOT: Developer Dashboard'da uygulamayı oluşturan Spotify hesabında Premium abonelik gerekir.
|
||||
SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_CLIENT_SECRET=
|
||||
SPOTIFY_ARTIST_ID=
|
||||
SPOTIFY_MARKET=TR
|
||||
|
||||
# YouTube Data API v3 (Müzik Prodüksiyonları / Art Track senkronizasyonu)
|
||||
# Ana kanal (@handle) — Videolar sekmesi
|
||||
YOUTUBE_CHANNEL_ID=
|
||||
# Topic kanalı — Yayınlananlar sekmesi (ZORUNLU önerilir, otomatik arama güvenilir değil)
|
||||
YOUTUBE_TOPIC_CHANNEL_ID=UCEGzDgiExoGrwWEnpIdOGRA
|
||||
# Sunucu/cron senkronizasyonu için KISITLAMASIZ veya IP kısıtlı ayrı bir anahtar kullanın.
|
||||
YOUTUBE_API_KEY=
|
||||
# İsteğe bağlı: playlist ID doğrudan (Topic uploads: UU + topic channel ID'den sonraki kısım)
|
||||
YOUTUBE_PLAYLIST_ID=
|
||||
# İsteğe bağlı: yalnızca DistroKid yayınları için açıklama filtresi
|
||||
# YOUTUBE_DISTRIBUTOR_FILTER=Provided to YouTube by DistroKid
|
||||
# Sadece referrer kısıtlı web anahtarı kullanıyorsanız:
|
||||
# YOUTUBE_API_REFERER=https://truncgil.com
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<img width="1920" height="518" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" />
|
||||
<img width="50%" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" />
|
||||
|
||||
# Citrus Platform
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\BlogTopic;
|
||||
use App\Models\Blog; // Ensure this model exists and has 'status' column
|
||||
use App\Models\User;
|
||||
use App\Services\AiBlogService;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class GenerateBlogAssistant extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'blog:generate-assistant {--topic= : Specific topic to generate}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate a new AI blog post draft based on active topics.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(AiBlogService $aiService)
|
||||
{
|
||||
\Illuminate\Support\Facades\Log::info('GenerateBlogAssistant triggered. Args: ' . json_encode($_SERVER['argv'] ?? []));
|
||||
$this->info('Starting AI Blog Assistant...');
|
||||
|
||||
// 1. Select Topic
|
||||
$topicName = $this->option('topic');
|
||||
$topic = null;
|
||||
|
||||
if ($topicName) {
|
||||
$topic = BlogTopic::where('name', $topicName)->first();
|
||||
if (!$topic) {
|
||||
$this->error("Topic '{$topicName}' not found.");
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
// Find oldest used active topic
|
||||
$topic = BlogTopic::active()
|
||||
->orderBy('last_used_at', 'asc') // Nulls first usually, or old dates
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$topic) {
|
||||
$this->warn('No active topics found. Please add topics to blog_topics table.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info("Selected Topic: {$topic->name}");
|
||||
|
||||
// 2. Generate Content
|
||||
$this->info('Generating content with Gemini API...');
|
||||
try {
|
||||
$contentData = $aiService->generateContent($topic->name, $topic->context);
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Content generation failed: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (empty($contentData) || !isset($contentData['title']) || !isset($contentData['content'])) {
|
||||
$this->error('Valid content not returned from AI Service.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info('Content generated successfully.');
|
||||
|
||||
// 3. Generate/Fetch Image
|
||||
$this->info('Fetching cover image...');
|
||||
$imagePath = null;
|
||||
if (isset($contentData['image_prompt'])) {
|
||||
$imagePath = $aiService->fetchAndSaveImage($topic->name, $contentData['image_title'] ?? $contentData['image_prompt']); // Pollinations uses prompt
|
||||
}
|
||||
|
||||
if (!$imagePath) {
|
||||
$this->warn('Could not fetch image, proceeding without cover image.');
|
||||
} else {
|
||||
$this->info("Image saved to: {$imagePath}");
|
||||
}
|
||||
|
||||
// 4. Create Draft Blog Post
|
||||
$author = User::first(); // Default to first user (Admin)
|
||||
|
||||
$blog = new Blog();
|
||||
$blog->title = $contentData['title'];
|
||||
$blog->slug = Str::slug($contentData['title']);
|
||||
$blog->content = $contentData['content'];
|
||||
$blog->excerpt = $contentData['excerpt'] ?? Str::limit(strip_tags($contentData['content']), 150);
|
||||
$blog->meta_title = $contentData['meta_title'] ?? $contentData['title'];
|
||||
$blog->meta_description = $contentData['meta_description'] ?? $blog->excerpt;
|
||||
$blog->status = 'draft'; // Verification required
|
||||
$blog->author_id = $author ? $author->id : 1;
|
||||
$blog->featured_image = $imagePath;
|
||||
$blog->published_at = null; // Draft
|
||||
$blog->tags = $contentData['tags'] ?? [];
|
||||
// category_id? We might need to guess or default.
|
||||
// For now leave null or set default if exists.
|
||||
|
||||
$blog->save();
|
||||
|
||||
// 5. Update Topic Usage
|
||||
$topic->last_used_at = Carbon::now();
|
||||
$topic->save();
|
||||
|
||||
$this->info("Blog post '{$blog->title}' created as DRAFT (ID: {$blog->id}).");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class ImportHtmlTemplates extends Command
|
||||
'span' => 'text.content',
|
||||
'address' => 'text.address',
|
||||
'button' => 'text.button',
|
||||
'a' => 'url.link',
|
||||
'a' => 'text.link_text',
|
||||
'li' => 'text.list_item',
|
||||
'label' => 'text.label',
|
||||
];
|
||||
@@ -260,7 +260,7 @@ class ImportHtmlTemplates extends Command
|
||||
$links = $xpath->query('.//a', $node);
|
||||
foreach ($links as $link) {
|
||||
$href = $link->getAttribute('href');
|
||||
if (empty($href) || str_starts_with($href, '#') || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue;
|
||||
if (empty($href) || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue;
|
||||
|
||||
$fixedHref = $this->fixAssetPath($href);
|
||||
if (str_ends_with($fixedHref, '.html')) {
|
||||
@@ -268,7 +268,7 @@ class ImportHtmlTemplates extends Command
|
||||
if($fixedHref == 'index') $fixedHref = '/';
|
||||
}
|
||||
|
||||
$key = $this->generateKey('url.link', $counts);
|
||||
$key = $this->generateKey('text.link_url', $counts);
|
||||
$data[$key] = $fixedHref;
|
||||
$link->setAttribute('href', "{" . $key . "}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\MusicProduction;
|
||||
use App\Services\SpotifyService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SyncSpotifyMusicProductions extends Command
|
||||
{
|
||||
protected $signature = 'spotify:sync-music-productions
|
||||
{--artist= : Override Spotify artist ID}
|
||||
{--dry-run : Show changes without writing to database}';
|
||||
|
||||
protected $description = 'Spotify sanatçı hesabından albüm ve single verilerini müzik prodüksiyonlarına senkronize eder';
|
||||
|
||||
public function handle(SpotifyService $spotifyService): int
|
||||
{
|
||||
if ($this->option('artist')) {
|
||||
$spotifyService = new SpotifyService(
|
||||
artistId: $this->option('artist'),
|
||||
);
|
||||
}
|
||||
|
||||
if (! $spotifyService->isConfigured()) {
|
||||
$this->error(__('music_productions.spotify_not_configured'));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info(__('music_productions.spotify_sync_started'));
|
||||
|
||||
try {
|
||||
$albumSummaries = $spotifyService->getArtistAlbums();
|
||||
} catch (\Throwable $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (empty($albumSummaries)) {
|
||||
$this->warn(__('music_productions.spotify_no_albums'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
foreach ($albumSummaries as $index => $albumSummary) {
|
||||
$albumId = $albumSummary['id'] ?? null;
|
||||
|
||||
if (blank($albumId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$album = $spotifyService->getAlbum($albumId) ?? $albumSummary;
|
||||
$title = $album['name'] ?? __('music_productions.spotify_untitled_album');
|
||||
$artistName = collect($album['artists'] ?? [])->pluck('name')->first();
|
||||
$releaseDate = $this->parseReleaseDate($album['release_date'] ?? null, $album['release_date_precision'] ?? 'day');
|
||||
$spotifyUrl = $album['external_urls']['spotify'] ?? null;
|
||||
$coverUrl = collect($album['images'] ?? [])->first()['url'] ?? null;
|
||||
$albumType = $album['album_type'] ?? 'album';
|
||||
|
||||
$production = $this->findMatchingProduction($albumId, $title);
|
||||
|
||||
$action = $production ? 'update' : 'create';
|
||||
$this->line(sprintf(
|
||||
'[%d/%d] %s: %s (%s)',
|
||||
$index + 1,
|
||||
count($albumSummaries),
|
||||
$action === 'create' ? 'Yeni' : 'Güncelle',
|
||||
$title,
|
||||
$releaseDate?->format('Y-m-d') ?? '-'
|
||||
));
|
||||
|
||||
if ($dryRun) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'spotify_album_id' => $albumId,
|
||||
'spotify_url' => $spotifyUrl,
|
||||
'spotify_cover_url' => $coverUrl,
|
||||
'spotify_type' => $albumType,
|
||||
'spotify_synced_at' => now(),
|
||||
];
|
||||
|
||||
if (! $production) {
|
||||
$attributes['title'] = $title;
|
||||
$attributes['slug'] = SpotifyService::uniqueSlug($title);
|
||||
$attributes['client_name'] = $artistName;
|
||||
$attributes['production_date'] = $releaseDate;
|
||||
$attributes['content'] = $spotifyService->buildDefaultContent($album);
|
||||
$attributes['is_active'] = true;
|
||||
$attributes['sort_order'] = 0;
|
||||
|
||||
$coverPath = $spotifyService->downloadCoverImage($coverUrl, $albumId);
|
||||
if ($coverPath) {
|
||||
$attributes['cover_image'] = $coverPath;
|
||||
}
|
||||
|
||||
MusicProduction::create($attributes);
|
||||
$created++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blank($production->title)) {
|
||||
$attributes['title'] = $title;
|
||||
}
|
||||
|
||||
if (blank($production->client_name) && filled($artistName)) {
|
||||
$attributes['client_name'] = $artistName;
|
||||
}
|
||||
|
||||
if (blank($production->production_date) && $releaseDate) {
|
||||
$attributes['production_date'] = $releaseDate;
|
||||
}
|
||||
|
||||
if (blank($production->content)) {
|
||||
$attributes['content'] = $spotifyService->buildDefaultContent($album);
|
||||
}
|
||||
|
||||
if (blank($production->cover_image) && filled($coverUrl)) {
|
||||
$coverPath = $spotifyService->downloadCoverImage($coverUrl, $albumId);
|
||||
if ($coverPath) {
|
||||
$attributes['cover_image'] = $coverPath;
|
||||
}
|
||||
}
|
||||
|
||||
$production->update($attributes);
|
||||
$updated++;
|
||||
|
||||
usleep(150000);
|
||||
}
|
||||
|
||||
$message = __('music_productions.spotify_sync_completed', [
|
||||
'created' => $created,
|
||||
'updated' => $updated,
|
||||
'total' => count($albumSummaries),
|
||||
]);
|
||||
|
||||
$this->info($message);
|
||||
Log::info('Spotify music productions sync completed', [
|
||||
'created' => $created,
|
||||
'updated' => $updated,
|
||||
'total' => count($albumSummaries),
|
||||
'dry_run' => $dryRun,
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function findMatchingProduction(string $albumId, string $title): ?MusicProduction
|
||||
{
|
||||
$bySpotifyId = MusicProduction::withTrashed()
|
||||
->where('spotify_album_id', $albumId)
|
||||
->first();
|
||||
|
||||
if ($bySpotifyId) {
|
||||
return $bySpotifyId->trashed() ? null : $bySpotifyId;
|
||||
}
|
||||
|
||||
$slug = Str::slug($title);
|
||||
|
||||
$bySlug = MusicProduction::query()
|
||||
->where('slug', $slug)
|
||||
->first();
|
||||
|
||||
if ($bySlug) {
|
||||
return $bySlug;
|
||||
}
|
||||
|
||||
return MusicProduction::query()
|
||||
->whereRaw('LOWER(title) = ?', [Str::lower($title)], 'and')
|
||||
->first();
|
||||
}
|
||||
|
||||
protected function parseReleaseDate(?string $date, string $precision = 'day'): ?Carbon
|
||||
{
|
||||
if (blank($date)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($precision) {
|
||||
'year' => Carbon::createFromFormat('Y', $date)->startOfYear(),
|
||||
'month' => Carbon::createFromFormat('Y-m', $date)->startOfMonth(),
|
||||
default => Carbon::parse($date),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\MusicProduction;
|
||||
use App\Services\YouTubeService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class SyncYoutubeReleases extends Command
|
||||
{
|
||||
protected $signature = 'youtube:sync-releases
|
||||
{--channel= : Override YouTube main channel ID}
|
||||
{--topic= : Override YouTube Topic channel ID (Yayınlananlar / art track)}
|
||||
{--playlist= : Override Releases playlist ID}
|
||||
{--dry-run : Show changes without writing to database}';
|
||||
|
||||
protected $description = 'YouTube kanalının Releases (Yayınlar) listesindeki art track kayıtlarını müzik prodüksiyonlarına senkronize eder';
|
||||
|
||||
public function handle(YouTubeService $youTubeService): int
|
||||
{
|
||||
if ($this->option('channel') || $this->option('topic') || $this->option('playlist')) {
|
||||
$youTubeService = new YouTubeService(
|
||||
channelId: $this->option('channel') ?? config('services.youtube.channel_id'),
|
||||
topicChannelId: $this->option('topic') ?? config('services.youtube.topic_channel_id'),
|
||||
playlistId: $this->option('playlist') ?? config('services.youtube.playlist_id'),
|
||||
);
|
||||
}
|
||||
|
||||
if (! $youTubeService->isConfigured()) {
|
||||
$this->error(__('music_productions.youtube_not_configured'));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info(__('music_productions.youtube_sync_started'));
|
||||
$this->line(__('music_productions.youtube_sync_source', [
|
||||
'source' => $youTubeService->getReleasesSourceLabel(),
|
||||
]));
|
||||
$this->line('Playlist ID: ' . $youTubeService->resolveReleasesPlaylistId());
|
||||
|
||||
try {
|
||||
$releases = $youTubeService->getReleases();
|
||||
} catch (\Throwable $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if (empty($releases)) {
|
||||
$this->warn(__('music_productions.youtube_no_releases'));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
|
||||
foreach ($releases as $index => $release) {
|
||||
$videoId = $release['video_id'] ?? null;
|
||||
|
||||
if (blank($videoId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$title = $release['title'] ?? __('music_productions.youtube_untitled_release');
|
||||
$releaseDate = filled($release['release_date'] ?? null)
|
||||
? Carbon::parse($release['release_date'])
|
||||
: null;
|
||||
$youtubeUrl = $release['youtube_url'] ?? null;
|
||||
$coverUrl = $release['cover_url'] ?? null;
|
||||
$description = $release['description'] ?? '';
|
||||
|
||||
$production = $this->findMatchingProduction($videoId, $title);
|
||||
|
||||
$action = $production ? 'update' : 'create';
|
||||
$this->line(sprintf(
|
||||
'[%d/%d] %s: %s (%s)',
|
||||
$index + 1,
|
||||
count($releases),
|
||||
$action === 'create' ? 'Yeni' : 'Güncelle',
|
||||
$title,
|
||||
$releaseDate?->format('Y-m-d') ?? '-'
|
||||
));
|
||||
|
||||
if ($dryRun) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = [
|
||||
'youtube_video_id' => $videoId,
|
||||
'youtube_url' => $youtubeUrl,
|
||||
'youtube_cover_url' => $coverUrl,
|
||||
'youtube_description' => $description,
|
||||
'youtube_synced_at' => now(),
|
||||
];
|
||||
|
||||
if (! $production) {
|
||||
$attributes['title'] = $title;
|
||||
$attributes['slug'] = YouTubeService::uniqueSlug($title);
|
||||
$attributes['production_date'] = $releaseDate;
|
||||
$attributes['content'] = $youTubeService->buildDefaultContent($release);
|
||||
$attributes['is_active'] = true;
|
||||
$attributes['sort_order'] = 0;
|
||||
|
||||
$coverPath = $youTubeService->downloadCoverImage($coverUrl, $videoId);
|
||||
if ($coverPath) {
|
||||
$attributes['cover_image'] = $coverPath;
|
||||
}
|
||||
|
||||
MusicProduction::create($attributes);
|
||||
$created++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blank($production->title)) {
|
||||
$attributes['title'] = $title;
|
||||
}
|
||||
|
||||
if ($releaseDate) {
|
||||
$attributes['production_date'] = $releaseDate;
|
||||
}
|
||||
|
||||
if (blank($production->content)) {
|
||||
$attributes['content'] = $youTubeService->buildDefaultContent($release);
|
||||
}
|
||||
|
||||
if (blank($production->cover_image) && filled($coverUrl)) {
|
||||
$coverPath = $youTubeService->downloadCoverImage($coverUrl, $videoId);
|
||||
if ($coverPath) {
|
||||
$attributes['cover_image'] = $coverPath;
|
||||
}
|
||||
}
|
||||
|
||||
$production->update($attributes);
|
||||
$updated++;
|
||||
|
||||
usleep(150000);
|
||||
}
|
||||
|
||||
$message = __('music_productions.youtube_sync_completed', [
|
||||
'created' => $created,
|
||||
'updated' => $updated,
|
||||
'total' => count($releases),
|
||||
]);
|
||||
|
||||
$this->info($message);
|
||||
Log::info('YouTube releases sync completed', [
|
||||
'created' => $created,
|
||||
'updated' => $updated,
|
||||
'total' => count($releases),
|
||||
'dry_run' => $dryRun,
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function findMatchingProduction(string $videoId, string $title): ?MusicProduction
|
||||
{
|
||||
$byYoutubeId = MusicProduction::withTrashed()
|
||||
->where('youtube_video_id', $videoId)
|
||||
->first();
|
||||
|
||||
if ($byYoutubeId) {
|
||||
return $byYoutubeId->trashed() ? null : $byYoutubeId;
|
||||
}
|
||||
|
||||
$slug = Str::slug($title);
|
||||
|
||||
$bySlug = MusicProduction::query()
|
||||
->whereNull('youtube_video_id', 'and', false)
|
||||
->where('slug', '=', $slug)
|
||||
->first();
|
||||
|
||||
if ($bySlug) {
|
||||
return $bySlug;
|
||||
}
|
||||
|
||||
return MusicProduction::query()
|
||||
->whereNull('youtube_video_id', 'and', false)
|
||||
->whereRaw('LOWER(title) = ?', [Str::lower($title)], 'and')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Awards;
|
||||
|
||||
use App\Filament\Admin\Resources\Awards\Pages\CreateAward;
|
||||
use App\Filament\Admin\Resources\Awards\Pages\EditAward;
|
||||
use App\Filament\Admin\Resources\Awards\Pages\ListAwards;
|
||||
use App\Filament\Admin\Resources\Awards\Schemas\AwardForm;
|
||||
use App\Filament\Admin\Resources\Awards\Tables\AwardsTable;
|
||||
use App\Models\Award;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class AwardResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Award::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-trophy';
|
||||
|
||||
protected static ?int $navigationSort = 46;
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('awards.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('awards.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('awards.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('awards.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return AwardForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return AwardsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListAwards::route('/'),
|
||||
'create' => CreateAward::route('/create'),
|
||||
'edit' => EditAward::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Awards\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Awards\AwardResource;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateAward extends CreateRecord
|
||||
{
|
||||
protected static string $resource = AwardResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('awards.create') ?? 'Yeni Ödül Ekle';
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getCreatedNotificationTitle(): ?string
|
||||
{
|
||||
return __('awards.created_successfully') ?? 'Ödül başarıyla eklendi.';
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
TranslationTabs::saveTranslations($this->record, $this->form->getState());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Awards\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Awards\AwardResource;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditAward extends EditRecord
|
||||
{
|
||||
protected static string $resource = AwardResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('awards.edit') ?? 'Ödülü Düzenle';
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->label(__('awards.delete') ?? 'Sil'),
|
||||
ForceDeleteAction::make()
|
||||
->label(__('awards.force_delete') ?? 'Kalıcı Olarak Sil'),
|
||||
RestoreAction::make()
|
||||
->label(__('awards.restore') ?? 'Geri Yükle'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getSavedNotificationTitle(): ?string
|
||||
{
|
||||
return __('awards.updated_successfully') ?? 'Ödül başarıyla güncellendi.';
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
TranslationTabs::saveTranslations($this->record, $this->form->getState());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Awards\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Awards\AwardResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListAwards extends ListRecords
|
||||
{
|
||||
protected static string $resource = AwardResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('awards.title') ?? 'Ödüllerimiz';
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label(__('awards.create') ?? 'Yeni Ödül Ekle'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Awards\Schemas;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class AwardForm
|
||||
{
|
||||
public static function categoryOptions(): array
|
||||
{
|
||||
return [
|
||||
'hackathon' => __('awards.category_hackathon') ?? 'Hackathon',
|
||||
'export' => __('awards.category_export') ?? 'İhracat',
|
||||
'innovation' => __('awards.category_innovation') ?? 'İnovasyon',
|
||||
'design' => __('awards.category_design') ?? 'Tasarım',
|
||||
'general' => __('awards.category_general') ?? 'Genel',
|
||||
];
|
||||
}
|
||||
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->columns(3)
|
||||
->schema([
|
||||
Section::make(__('awards.content_section') ?? 'Ödül İçeriği')
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label(__('awards.title_field') ?? 'Ödül Başlığı (Varsayılan)')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('issuer')
|
||||
->label(__('awards.issuer_field') ?? 'Ödülü Veren Kurum (Varsayılan)')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Textarea::make('description')
|
||||
->label(__('awards.description_field') ?? 'Açıklama (Varsayılan)')
|
||||
->required()
|
||||
->rows(5)
|
||||
->columnSpanFull(),
|
||||
|
||||
TranslationTabs::make([
|
||||
'title' => [
|
||||
'type' => 'text',
|
||||
'label' => __('awards.title_field') ?? 'Ödül Başlığı',
|
||||
'required' => false,
|
||||
'maxLength' => 255,
|
||||
],
|
||||
'issuer' => [
|
||||
'type' => 'text',
|
||||
'label' => __('awards.issuer_field') ?? 'Ödülü Veren Kurum',
|
||||
'required' => false,
|
||||
'maxLength' => 255,
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'textarea',
|
||||
'label' => __('awards.description_field') ?? 'Açıklama',
|
||||
'required' => false,
|
||||
'rows' => 5,
|
||||
],
|
||||
]),
|
||||
])
|
||||
->columnSpan(2),
|
||||
|
||||
Section::make(__('awards.settings_section') ?? 'Ödül Ayarları')
|
||||
->schema([
|
||||
FileUpload::make('image')
|
||||
->label(__('awards.image_field') ?? 'Ödül Görseli / Logo')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('awards')
|
||||
->required(),
|
||||
|
||||
DatePicker::make('award_date')
|
||||
->label(__('awards.date_field') ?? 'Ödül Tarihi')
|
||||
->required(),
|
||||
|
||||
Select::make('category')
|
||||
->label(__('awards.category_field') ?? 'Kategori')
|
||||
->options(self::categoryOptions())
|
||||
->default('general')
|
||||
->required()
|
||||
->native(false),
|
||||
|
||||
TextInput::make('external_link')
|
||||
->label(__('awards.link_field') ?? 'Doğrulama / Haber Linki')
|
||||
->url()
|
||||
->maxLength(255),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->label(__('awards.sort_order_field') ?? 'Sıralama')
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(0),
|
||||
|
||||
Toggle::make('is_featured')
|
||||
->label(__('awards.is_featured_field') ?? 'Öne Çıkarılan Ödül')
|
||||
->default(false),
|
||||
|
||||
Toggle::make('is_active')
|
||||
->label(__('awards.is_active_field') ?? 'Aktif / Görünür')
|
||||
->default(true),
|
||||
])
|
||||
->columnSpan(1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Awards\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class AwardsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
ImageColumn::make('image')
|
||||
->label(__('awards.table_image') ?? 'Logo')
|
||||
->disk('public')
|
||||
->square()
|
||||
->size(50),
|
||||
|
||||
TextColumn::make('title')
|
||||
->label(__('awards.table_title') ?? 'Ödül Başlığı')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(40),
|
||||
|
||||
TextColumn::make('issuer')
|
||||
->label(__('awards.table_issuer') ?? 'Veren Kurum')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('award_date')
|
||||
->label(__('awards.table_date') ?? 'Ödül Tarihi')
|
||||
->date('d.m.Y')
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('category')
|
||||
->label(__('awards.table_category') ?? 'Kategori')
|
||||
->badge()
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'hackathon' => __('awards.category_hackathon') ?? 'Hackathon',
|
||||
'export' => __('awards.category_export') ?? 'İhracat',
|
||||
'innovation' => __('awards.category_innovation') ?? 'İnovasyon',
|
||||
'design' => __('awards.category_design') ?? 'Tasarım',
|
||||
default => __('awards.category_general') ?? 'Genel',
|
||||
})
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
ToggleColumn::make('is_featured')
|
||||
->label(__('awards.table_is_featured') ?? 'Öne Çıkan')
|
||||
->alignCenter(),
|
||||
|
||||
ToggleColumn::make('is_active')
|
||||
->label(__('awards.table_is_active') ?? 'Aktif')
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('awards.sort_order_field') ?? 'Sıra')
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
])
|
||||
->filters([
|
||||
TernaryFilter::make('is_active')
|
||||
->label(__('awards.is_active_field') ?? 'Aktiflik Durumu'),
|
||||
TernaryFilter::make('is_featured')
|
||||
->label(__('awards.is_featured_field') ?? 'Öne Çıkarılma Durumu'),
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->label(__('awards.edit') ?? 'Düzenle'),
|
||||
])
|
||||
->actions([
|
||||
// Individual actions can be put here if necessary
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->label(__('awards.delete') ?? 'Sil'),
|
||||
RestoreBulkAction::make()
|
||||
->label(__('awards.restore') ?? 'Geri Yükle'),
|
||||
ForceDeleteBulkAction::make()
|
||||
->label(__('awards.force_delete') ?? 'Kalıcı Olarak Sil'),
|
||||
]),
|
||||
])
|
||||
->defaultSort('sort_order', 'asc')
|
||||
->reorderable('sort_order');
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ class BlogCategoryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = BlogCategory::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-tag';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Blog';
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class BlogResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Blog::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedDocumentText;
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\CreateCareerApplication;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\EditCareerApplication;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\ListCareerApplications;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Schemas\CareerApplicationForm;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Tables\CareerApplicationsTable;
|
||||
use App\Models\CareerApplication;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CareerApplicationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CareerApplication::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-user-group';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('career.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('career.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('career.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return CareerApplicationForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return CareerApplicationsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListCareerApplications::route('/'),
|
||||
'create' => CreateCareerApplication::route('/create'),
|
||||
'edit' => EditCareerApplication::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCareerApplication extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCareerApplication extends EditRecord
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCareerApplications extends ListRecords
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Schemas;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Illuminate\Support\Str;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\View;
|
||||
|
||||
class CareerApplicationForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('Kişisel ve Başvuru Bilgileri')
|
||||
->icon('heroicon-m-user')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label(__('career.name'))
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('email')
|
||||
->label(__('career.email'))
|
||||
->email()
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
TextInput::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->disabled(),
|
||||
|
||||
Select::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
])
|
||||
->required(),
|
||||
|
||||
Textarea::make('message')
|
||||
->label(__('career.message'))
|
||||
->disabled()
|
||||
->columnSpanFull(),
|
||||
])->columns(2),
|
||||
|
||||
Tab::make('Staj Belgeleri & Giriş Bilgileri')
|
||||
->icon('heroicon-m-document-text')
|
||||
->schema([
|
||||
FileUpload::make('cv_path')
|
||||
->label(__('career.cv'))
|
||||
->disk('public')
|
||||
->directory('cvs')
|
||||
->required()
|
||||
->disabled()
|
||||
->downloadable(),
|
||||
|
||||
TextInput::make('username')
|
||||
->label('Kullanıcı Adı')
|
||||
->unique(ignoreRecord: true)
|
||||
->nullable(),
|
||||
|
||||
TextInput::make('password')
|
||||
->label('Şifre')
|
||||
->password()
|
||||
->revealable()
|
||||
->dehydrateStateUsing(fn ($state) => filled($state) ? Hash::make($state) : null)
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->placeholder('Şifreyi değiştirmek istemiyorsanız boş bırakın')
|
||||
->nullable()
|
||||
->suffixAction(
|
||||
Action::make('generatePassword')
|
||||
->icon('heroicon-m-arrow-path')
|
||||
->action(fn (Set $set) => $set('password', Str::random(12)))
|
||||
),
|
||||
|
||||
FileUpload::make('to_be_signed_internship_form_path')
|
||||
->label('İmzalanacak Staj Formu (Stajyerden)')
|
||||
->disk('public')
|
||||
->directory('to_be_signed_interns')
|
||||
->downloadable()
|
||||
->nullable(),
|
||||
|
||||
FileUpload::make('signed_internship_form_path')
|
||||
->label('İmzalı Staj Formu')
|
||||
->disk('public')
|
||||
->directory('signed_interns')
|
||||
->downloadable()
|
||||
->nullable(),
|
||||
|
||||
DatePicker::make('internship_start_date')
|
||||
->label('Staj Başlangıç Tarihi')
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, $get, Set $set) {
|
||||
if ($state && $get('internship_total_days')) {
|
||||
self::calculateEndDate($state, $get('internship_total_days'), $set);
|
||||
} elseif ($state && $get('internship_end_date')) {
|
||||
self::calculateTotalDays($state, $get('internship_end_date'), $set);
|
||||
}
|
||||
})
|
||||
->nullable(),
|
||||
|
||||
DatePicker::make('internship_end_date')
|
||||
->label('Staj Bitiş Tarihi')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateTotalDays($get('internship_start_date'), $state, $set))
|
||||
->nullable(),
|
||||
|
||||
TextInput::make('internship_total_days')
|
||||
->label('Toplam Staj Süresi (İş Günü)')
|
||||
->numeric()
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, $get, Set $set) => self::calculateEndDate($get('internship_start_date'), $state, $set))
|
||||
->nullable(),
|
||||
])->columns(2),
|
||||
|
||||
Tab::make('Staj Günlüğü')
|
||||
->icon('heroicon-m-squares-plus')
|
||||
->schema([
|
||||
TextInput::make('github_repo')
|
||||
->label('GitHub Depo URL\'si')
|
||||
->url()
|
||||
->nullable()
|
||||
->live(),
|
||||
|
||||
// View::make('filament.components.intern-journal-timeline')
|
||||
// ->columnSpanFull()
|
||||
])
|
||||
])->columnSpanFull()
|
||||
]);
|
||||
}
|
||||
|
||||
public static function calculateTotalDays($start, $end, Set $set): void
|
||||
{
|
||||
if (!$start || !$end) {
|
||||
$set('internship_total_days', null);
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = \Carbon\Carbon::parse($start);
|
||||
$endDate = \Carbon\Carbon::parse($end);
|
||||
|
||||
if ($startDate->gt($endDate)) {
|
||||
$set('internship_total_days', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
$days = 0;
|
||||
while ($startDate->lte($endDate)) {
|
||||
if (!$startDate->isWeekend()) {
|
||||
$days++;
|
||||
}
|
||||
$startDate->addDay();
|
||||
}
|
||||
|
||||
$set('internship_total_days', $days);
|
||||
}
|
||||
|
||||
public static function calculateEndDate($start, $totalDays, Set $set): void
|
||||
{
|
||||
if (!$start || !$totalDays || $totalDays <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$startDate = \Carbon\Carbon::parse($start);
|
||||
$daysToAdd = intval($totalDays);
|
||||
|
||||
$endDate = $startDate->copy();
|
||||
$count = 0;
|
||||
$temp = $startDate->copy();
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
$endDate = $temp->copy();
|
||||
$temp->addDay();
|
||||
$count++;
|
||||
}
|
||||
|
||||
$set('internship_end_date', $endDate->format('Y-m-d'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Tables;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CareerApplicationsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(__('career.name'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('email')
|
||||
->label(__('career.email'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('type')
|
||||
->label(__('career.type'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'job' => 'success',
|
||||
'internship' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label(__('career.status'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'gray',
|
||||
'reviewed' => 'info',
|
||||
'rejected' => 'danger',
|
||||
'accepted' => 'success',
|
||||
'waiting_document' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
|
||||
|
||||
TextColumn::make('git_knowledge')
|
||||
->label(__('career.git_knowledge'))
|
||||
->badge()
|
||||
->color(fn ($state) => $state ? 'success' : 'danger')
|
||||
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
|
||||
|
||||
TextColumn::make('ai_usage')
|
||||
->label(__('career.ai_usage'))
|
||||
->badge()
|
||||
->color(fn ($state) => $state ? 'success' : 'danger')
|
||||
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('career.created_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => __('career.pending'),
|
||||
'reviewed' => __('career.reviewed'),
|
||||
'rejected' => __('career.rejected'),
|
||||
'accepted' => __('career.accepted'),
|
||||
'waiting_document' => __('career.waiting_document'),
|
||||
]),
|
||||
SelectFilter::make('type')
|
||||
->label(__('career.type'))
|
||||
->options([
|
||||
'job' => __('career.job'),
|
||||
'internship' => __('career.internship'),
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
Action::make('download_cv')
|
||||
->label(__('career.download_cv'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_signed_form')
|
||||
->label('İmzalı Form İndir')
|
||||
->icon('heroicon-o-document-check')
|
||||
->url(fn ($record) => $record->signed_internship_form_path ? Storage::disk('public')->url($record->signed_internship_form_path) : null)
|
||||
->visible(fn ($record) => !empty($record->signed_internship_form_path))
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_nda')
|
||||
->label(__('career.nda'))
|
||||
->icon('heroicon-o-shield-check')
|
||||
->url(fn ($record) => $record->nda_path ? Storage::disk('public')->url($record->nda_path) : null)
|
||||
->visible(fn ($record) => $record->nda_path !== null)
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_contract')
|
||||
->label(__('career.contract'))
|
||||
->icon('heroicon-o-document-text')
|
||||
->url(fn ($record) => $record->contract_path ? Storage::disk('public')->url($record->contract_path) : null)
|
||||
->visible(fn ($record) => $record->contract_path !== null)
|
||||
->openUrlInNewTab(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CompanyHistoryItems;
|
||||
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\Pages\CreateCompanyHistoryItem;
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\Pages\EditCompanyHistoryItem;
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\Pages\ListCompanyHistoryItems;
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\Schemas\CompanyHistoryItemForm;
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\Tables\CompanyHistoryItemsTable;
|
||||
use App\Models\CompanyHistoryItem;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class CompanyHistoryItemResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CompanyHistoryItem::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-clock';
|
||||
|
||||
protected static ?int $navigationSort = 45;
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('company_history.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('company_history.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('company_history.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('company_history.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return CompanyHistoryItemForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return CompanyHistoryItemsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListCompanyHistoryItems::route('/'),
|
||||
'create' => CreateCompanyHistoryItem::route('/create'),
|
||||
'edit' => EditCompanyHistoryItem::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\CompanyHistoryItemResource;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCompanyHistoryItem extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CompanyHistoryItemResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('company_history.create');
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getCreatedNotificationTitle(): ?string
|
||||
{
|
||||
return __('company_history.created_successfully');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
if (empty($data['position'])) {
|
||||
$data['position'] = null;
|
||||
}
|
||||
|
||||
unset($data['color_custom']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
TranslationTabs::saveTranslations($this->record, $this->form->getState());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\CompanyHistoryItemResource;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCompanyHistoryItem extends EditRecord
|
||||
{
|
||||
protected static string $resource = CompanyHistoryItemResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('company_history.edit');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make()
|
||||
->label(__('company_history.delete')),
|
||||
ForceDeleteAction::make()
|
||||
->label(__('company_history.force_delete')),
|
||||
RestoreAction::make()
|
||||
->label(__('company_history.restore')),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getSavedNotificationTitle(): ?string
|
||||
{
|
||||
return __('company_history.updated_successfully');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
if (empty($data['position'])) {
|
||||
$data['position'] = null;
|
||||
}
|
||||
|
||||
unset($data['color_custom']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
TranslationTabs::saveTranslations($this->record, $this->form->getState());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CompanyHistoryItems\CompanyHistoryItemResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCompanyHistoryItems extends ListRecords
|
||||
{
|
||||
protected static string $resource = CompanyHistoryItemResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('company_history.title');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label(__('company_history.create')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Schemas;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class CompanyHistoryItemForm
|
||||
{
|
||||
public static function iconOptions(): array
|
||||
{
|
||||
return [
|
||||
'heroicon-o-building-office-2' => __('company_history.icon_building'),
|
||||
'heroicon-o-academic-cap' => __('company_history.icon_academic'),
|
||||
'heroicon-o-globe-alt' => __('company_history.icon_globe'),
|
||||
'heroicon-o-users' => __('company_history.icon_users'),
|
||||
'heroicon-o-trophy' => __('company_history.icon_trophy'),
|
||||
'heroicon-o-map' => __('company_history.icon_map'),
|
||||
'heroicon-o-light-bulb' => __('company_history.icon_light_bulb'),
|
||||
'heroicon-o-rocket-launch' => __('company_history.icon_rocket'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function quarterOptions(): array
|
||||
{
|
||||
return [
|
||||
'Q1' => __('company_history.quarter_q1'),
|
||||
'Q2' => __('company_history.quarter_q2'),
|
||||
'Q3' => __('company_history.quarter_q3'),
|
||||
'Q4' => __('company_history.quarter_q4'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function colorPresets(): array
|
||||
{
|
||||
return [
|
||||
'#17a2b8' => __('company_history.color_teal'),
|
||||
'#f5a623' => __('company_history.color_orange'),
|
||||
'#e91e63' => __('company_history.color_pink'),
|
||||
'#2c3e50' => __('company_history.color_navy'),
|
||||
'#5dade2' => __('company_history.color_blue'),
|
||||
'#27ae60' => __('company_history.color_green'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->columns(3)
|
||||
->schema([
|
||||
Section::make(__('company_history.content_section'))
|
||||
->schema([
|
||||
TextInput::make('year')
|
||||
->label(__('company_history.year_field'))
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(1900)
|
||||
->maxValue(2100),
|
||||
|
||||
Select::make('quarter')
|
||||
->label(__('company_history.quarter_field'))
|
||||
->options(self::quarterOptions())
|
||||
->default('Q1')
|
||||
->required()
|
||||
->native(false),
|
||||
|
||||
TextInput::make('title')
|
||||
->label(__('company_history.title_field'))
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Textarea::make('content')
|
||||
->label(__('company_history.content_field'))
|
||||
->required()
|
||||
->rows(5)
|
||||
->columnSpanFull(),
|
||||
|
||||
TranslationTabs::make([
|
||||
'title' => [
|
||||
'type' => 'text',
|
||||
'label' => __('company_history.title_field'),
|
||||
'required' => false,
|
||||
'maxLength' => 255,
|
||||
],
|
||||
'content' => [
|
||||
'type' => 'textarea',
|
||||
'label' => __('company_history.content_field'),
|
||||
'required' => false,
|
||||
'rows' => 5,
|
||||
],
|
||||
]),
|
||||
])
|
||||
->columnSpan(2)
|
||||
->collapsible(false),
|
||||
|
||||
Section::make(__('company_history.settings_section'))
|
||||
->schema([
|
||||
Select::make('color')
|
||||
->label(__('company_history.color_field'))
|
||||
->options(self::colorPresets())
|
||||
->default('#17a2b8')
|
||||
->required()
|
||||
->native(false)
|
||||
->searchable(),
|
||||
|
||||
Select::make('icon')
|
||||
->label(__('company_history.icon_field'))
|
||||
->options(self::iconOptions())
|
||||
->default('heroicon-o-building-office-2')
|
||||
->required()
|
||||
->native(false)
|
||||
->searchable(),
|
||||
|
||||
Select::make('position')
|
||||
->label(__('company_history.position_field'))
|
||||
->options([
|
||||
'' => __('company_history.position_auto'),
|
||||
'left' => __('company_history.position_left'),
|
||||
'right' => __('company_history.position_right'),
|
||||
])
|
||||
->default(null),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->label(__('company_history.sort_order_field'))
|
||||
->numeric()
|
||||
->default(0)
|
||||
->minValue(0),
|
||||
|
||||
Checkbox::make('is_active')
|
||||
->label(__('company_history.is_active_field'))
|
||||
->default(true),
|
||||
])
|
||||
->columnSpan(1),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CompanyHistoryItems\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Tables\Columns\ColorColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CompanyHistoryItemsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('year')
|
||||
->label(__('company_history.table_year'))
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('quarter')
|
||||
->label(__('company_history.quarter_field'))
|
||||
->badge()
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
ColorColumn::make('color')
|
||||
->label(__('company_history.color_field')),
|
||||
|
||||
TextColumn::make('title')
|
||||
->label(__('company_history.table_title'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(40),
|
||||
|
||||
TextColumn::make('content')
|
||||
->label(__('company_history.content_field'))
|
||||
->limit(60)
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('position')
|
||||
->label(__('company_history.position_field'))
|
||||
->formatStateUsing(fn (?string $state): string => match ($state) {
|
||||
'left' => __('company_history.position_left'),
|
||||
'right' => __('company_history.position_right'),
|
||||
default => __('company_history.position_auto'),
|
||||
})
|
||||
->toggleable(),
|
||||
|
||||
ToggleColumn::make('is_active')
|
||||
->label(__('company_history.table_is_active'))
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('company_history.sort_order_field'))
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('company_history.table_created_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->label(__('company_history.table_updated_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
TernaryFilter::make('is_active')
|
||||
->label(__('company_history.is_active_field')),
|
||||
TrashedFilter::make(),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->label(__('company_history.edit')),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->label(__('company_history.delete')),
|
||||
RestoreBulkAction::make()
|
||||
->label(__('company_history.restore')),
|
||||
ForceDeleteBulkAction::make()
|
||||
->label(__('company_history.force_delete')),
|
||||
]),
|
||||
])
|
||||
->defaultSort('year', 'asc')
|
||||
->reorderable('sort_order');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MusicProductions;
|
||||
|
||||
use App\Filament\Admin\Resources\MusicProductions\Pages\CreateMusicProduction;
|
||||
use App\Filament\Admin\Resources\MusicProductions\Pages\EditMusicProduction;
|
||||
use App\Filament\Admin\Resources\MusicProductions\Pages\ListMusicProductions;
|
||||
use App\Filament\Admin\Resources\MusicProductions\Schemas\MusicProductionForm;
|
||||
use App\Filament\Admin\Resources\MusicProductions\Tables\MusicProductionsTable;
|
||||
use App\Models\MusicProduction;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class MusicProductionResource extends Resource
|
||||
{
|
||||
protected static ?string $model = MusicProduction::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-musical-note';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('music_productions.title');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('music_productions.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('music_productions.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return MusicProductionForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return MusicProductionsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListMusicProductions::route('/'),
|
||||
'create' => CreateMusicProduction::route('/create'),
|
||||
'edit' => EditMusicProduction::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MusicProductions\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MusicProductions\MusicProductionResource;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateMusicProduction extends CreateRecord
|
||||
{
|
||||
protected static string $resource = MusicProductionResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('music_productions.create');
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getCreatedNotificationTitle(): ?string
|
||||
{
|
||||
return __('music_productions.created_successfully');
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
// Save translations
|
||||
TranslationTabs::saveTranslations($this->record, $this->form->getState());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MusicProductions\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MusicProductions\MusicProductionResource;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditMusicProduction extends EditRecord
|
||||
{
|
||||
protected static string $resource = MusicProductionResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('music_productions.edit');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('save')
|
||||
->label(__('music_productions.save'))
|
||||
->action('save')
|
||||
->keyBindings(['mod+s'])
|
||||
->color('primary')
|
||||
->size('sm'),
|
||||
Action::make('cancel')
|
||||
->label(__('music_productions.cancel'))
|
||||
->url($this->getResource()::getUrl('index'))
|
||||
->color('gray')
|
||||
->size('sm'),
|
||||
DeleteAction::make()
|
||||
->label(__('music_productions.delete'))
|
||||
->size('sm'),
|
||||
RestoreAction::make()
|
||||
->label(__('music_productions.restore'))
|
||||
->size('sm'),
|
||||
ForceDeleteAction::make()
|
||||
->label(__('music_productions.force_delete'))
|
||||
->size('sm'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFormActions(): array
|
||||
{
|
||||
return []; // Hide standard bottom save/cancel buttons
|
||||
}
|
||||
|
||||
protected function getRedirectUrl(): string
|
||||
{
|
||||
return $this->getResource()::getUrl('index');
|
||||
}
|
||||
|
||||
protected function getSavedNotificationTitle(): ?string
|
||||
{
|
||||
return __('music_productions.updated_successfully');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
// Load existing translations
|
||||
$translationData = TranslationTabs::fillFromRecord($this->record);
|
||||
|
||||
\Log::info('Loading translations for music production edit', [
|
||||
'production_id' => $this->record->id,
|
||||
'translations' => $translationData
|
||||
]);
|
||||
|
||||
return array_merge($data, $translationData);
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
\Log::info('Saving music production translations', [
|
||||
'production_id' => $this->record->id,
|
||||
'form_state' => $this->form->getState()
|
||||
]);
|
||||
|
||||
// Save translations
|
||||
TranslationTabs::saveTranslations($this->record, $this->form->getState());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MusicProductions\Pages;
|
||||
|
||||
use App\Console\Commands\SyncSpotifyMusicProductions;
|
||||
use App\Console\Commands\SyncYoutubeReleases;
|
||||
use App\Filament\Admin\Resources\MusicProductions\MusicProductionResource;
|
||||
use App\Services\SpotifyService;
|
||||
use App\Services\YouTubeService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class ListMusicProductions extends ListRecords
|
||||
{
|
||||
protected static string $resource = MusicProductionResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('music_productions.title');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('syncSpotify')
|
||||
->label(__('music_productions.sync_spotify'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('success')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('music_productions.sync_spotify_heading'))
|
||||
->modalDescription(__('music_productions.sync_spotify_description'))
|
||||
->visible(fn (): bool => app(SpotifyService::class)->isConfigured())
|
||||
->action(function (): void {
|
||||
$exitCode = Artisan::call(SyncSpotifyMusicProductions::class);
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
Notification::make()
|
||||
->title(__('music_productions.spotify_sync_failed'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(__('music_productions.spotify_sync_success'))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
Action::make('syncYoutube')
|
||||
->label(__('music_productions.sync_youtube'))
|
||||
->icon('heroicon-o-play')
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('music_productions.sync_youtube_heading'))
|
||||
->modalDescription(__('music_productions.sync_youtube_description'))
|
||||
->visible(fn (): bool => app(YouTubeService::class)->isConfigured())
|
||||
->action(function (): void {
|
||||
$exitCode = Artisan::call(SyncYoutubeReleases::class);
|
||||
|
||||
if ($exitCode !== 0) {
|
||||
Notification::make()
|
||||
->title(__('music_productions.youtube_sync_failed'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(__('music_productions.youtube_sync_success'))
|
||||
->success()
|
||||
->send();
|
||||
}),
|
||||
CreateAction::make()
|
||||
->label(__('music_productions.create')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MusicProductions\Schemas;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Models\MusicProduction;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MusicProductionForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->columns(3)
|
||||
->schema([
|
||||
// Sol Kolon - Ana İçerik (2 sütun genişliğinde)
|
||||
Section::make(__('music_productions.content_section'))
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label(__('music_productions.title_field'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, $state, callable $set) {
|
||||
if ($operation !== 'create') {
|
||||
return;
|
||||
}
|
||||
$set('slug', Str::slug($state));
|
||||
}),
|
||||
|
||||
TextInput::make('slug')
|
||||
->label(__('music_productions.slug_field'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->rules(['alpha_dash'])
|
||||
->helperText(__('music_productions.slug_helper')),
|
||||
|
||||
TextInput::make('client_name')
|
||||
->label(__('music_productions.client_name_field'))
|
||||
->maxLength(255),
|
||||
|
||||
RichEditor::make('content')
|
||||
->label(__('music_productions.content_field'))
|
||||
->fileAttachmentsDisk('public')
|
||||
->fileAttachmentsDirectory('music-productions')
|
||||
->fileAttachmentsVisibility('public')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpan(2)
|
||||
->collapsible(false),
|
||||
|
||||
// Sağ Kolon - Ayarlar (1 sütun genişliğinde)
|
||||
Section::make(__('music_productions.settings_section'))
|
||||
->schema([
|
||||
FileUpload::make('cover_image')
|
||||
->label(__('music_productions.cover_image_field'))
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('music-productions/covers')
|
||||
->visibility('public')
|
||||
->imageEditor()
|
||||
->imageEditorAspectRatios([
|
||||
'16:9',
|
||||
'4:3',
|
||||
'1:1',
|
||||
])
|
||||
->helperText(__('music_productions.cover_image_helper'))
|
||||
->columnSpanFull(),
|
||||
|
||||
DatePicker::make('production_date')
|
||||
->label(__('music_productions.production_date_field'))
|
||||
->displayFormat('d.m.Y')
|
||||
->helperText(__('music_productions.production_date_helper'))
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->label(__('music_productions.sort_order_field'))
|
||||
->numeric()
|
||||
->default(0)
|
||||
->columnSpanFull(),
|
||||
|
||||
Checkbox::make('is_active')
|
||||
->label(__('music_productions.is_active_field'))
|
||||
->default(true)
|
||||
->helperText(__('music_productions.is_active_helper'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpan(1)
|
||||
->collapsible(false),
|
||||
|
||||
Section::make(__('music_productions.spotify_section'))
|
||||
->schema([
|
||||
Placeholder::make('spotify_album_id_display')
|
||||
->label(__('music_productions.spotify_album_id_field'))
|
||||
->content(fn (?MusicProduction $record): string => $record?->spotify_album_id ?? '-'),
|
||||
|
||||
Placeholder::make('spotify_type_display')
|
||||
->label(__('music_productions.spotify_type_field'))
|
||||
->content(fn (?MusicProduction $record): string => $record?->spotify_type
|
||||
? __('music_productions.spotify_type_' . $record->spotify_type, [], $record->spotify_type)
|
||||
: '-'),
|
||||
|
||||
Placeholder::make('spotify_url_display')
|
||||
->label(__('music_productions.spotify_url_field'))
|
||||
->content(fn (?MusicProduction $record): string => $record?->spotify_url ?? '-'),
|
||||
|
||||
Placeholder::make('spotify_synced_at_display')
|
||||
->label(__('music_productions.spotify_synced_at_field'))
|
||||
->content(fn (?MusicProduction $record): string => $record?->spotify_synced_at
|
||||
?->timezone(config('app.timezone'))
|
||||
->format('d.m.Y H:i') ?? '-'),
|
||||
])
|
||||
->columnSpanFull()
|
||||
->collapsible(true)
|
||||
->collapsed(true)
|
||||
->visible(fn (?MusicProduction $record): bool => filled($record?->spotify_album_id)),
|
||||
|
||||
Section::make(__('music_productions.youtube_section'))
|
||||
->schema([
|
||||
Placeholder::make('youtube_video_id_display')
|
||||
->label(__('music_productions.youtube_video_id_field'))
|
||||
->content(fn (?MusicProduction $record): string => $record?->youtube_video_id ?? '-'),
|
||||
|
||||
Placeholder::make('youtube_url_display')
|
||||
->label(__('music_productions.youtube_url_field'))
|
||||
->content(fn (?MusicProduction $record): string => $record?->youtube_url ?? '-'),
|
||||
|
||||
Placeholder::make('youtube_description_display')
|
||||
->label(__('music_productions.youtube_description_field'))
|
||||
->content(fn (?MusicProduction $record): string => filled($record?->youtube_description)
|
||||
? \Illuminate\Support\Str::limit($record->youtube_description, 500)
|
||||
: '-'),
|
||||
|
||||
Placeholder::make('youtube_synced_at_display')
|
||||
->label(__('music_productions.youtube_synced_at_field'))
|
||||
->content(fn (?MusicProduction $record): string => $record?->youtube_synced_at
|
||||
?->timezone(config('app.timezone'))
|
||||
->format('d.m.Y H:i') ?? '-'),
|
||||
])
|
||||
->columnSpanFull()
|
||||
->collapsible(true)
|
||||
->collapsed(true)
|
||||
->visible(fn (?MusicProduction $record): bool => filled($record?->youtube_video_id)),
|
||||
|
||||
// Alt Kısım - Galeri Görselleri
|
||||
Section::make(__('music_productions.gallery_field'))
|
||||
->schema([
|
||||
FileUpload::make('gallery')
|
||||
->label(__('music_productions.gallery_field'))
|
||||
->multiple()
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('music-productions/gallery')
|
||||
->visibility('public')
|
||||
->helperText(__('music_productions.gallery_helper'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpanFull()
|
||||
->collapsible(true)
|
||||
->collapsed(false),
|
||||
|
||||
// Çeviriler Kısımı (Çoklu Dil)
|
||||
Section::make('🌍 ' . __('music_productions.translations_section'))
|
||||
->schema([
|
||||
TranslationTabs::make([
|
||||
'title' => [
|
||||
'type' => 'text',
|
||||
'label' => __('music_productions.title_field'),
|
||||
'required' => false,
|
||||
'maxLength' => 255,
|
||||
],
|
||||
'client_name' => [
|
||||
'type' => 'text',
|
||||
'label' => __('music_productions.client_name_field'),
|
||||
'required' => false,
|
||||
'maxLength' => 255,
|
||||
],
|
||||
'content' => [
|
||||
'type' => 'richtext',
|
||||
'label' => __('music_productions.content_field'),
|
||||
'required' => false,
|
||||
],
|
||||
]),
|
||||
])
|
||||
->columnSpanFull()
|
||||
->collapsible(true)
|
||||
->collapsed(false),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MusicProductions\Tables;
|
||||
|
||||
use App\Models\MusicProduction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class MusicProductionsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
ImageColumn::make('cover_image')
|
||||
->label(__('music_productions.cover_image_field'))
|
||||
->getStateUsing(fn (MusicProduction $record): ?string => $record->cover_image_url)
|
||||
->square()
|
||||
->size(60),
|
||||
|
||||
TextColumn::make('title')
|
||||
->label(__('music_productions.table_title'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(50),
|
||||
|
||||
TextColumn::make('slug')
|
||||
->label(__('music_productions.table_slug'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('client_name')
|
||||
->label(__('music_productions.table_client_name'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('production_date')
|
||||
->label(__('music_productions.table_production_date'))
|
||||
->date('d.m.Y')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('spotify_type')
|
||||
->label(__('music_productions.spotify_type_field'))
|
||||
->badge()
|
||||
->formatStateUsing(fn (?string $state): string => filled($state)
|
||||
? __('music_productions.spotify_type_' . $state, [], $state)
|
||||
: '-')
|
||||
->color(fn (?string $state): string => match ($state) {
|
||||
'single' => 'info',
|
||||
'album' => 'success',
|
||||
default => 'gray',
|
||||
})
|
||||
->toggleable(),
|
||||
|
||||
ToggleColumn::make('is_active')
|
||||
->label(__('music_productions.table_is_active'))
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('music_productions.sort_order_field'))
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('music_productions.table_created_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('updated_at')
|
||||
->label(__('music_productions.table_updated_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
TernaryFilter::make('is_active')
|
||||
->label(__('music_productions.is_active_field')),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->label(__('music_productions.edit')),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->label(__('music_productions.delete')),
|
||||
RestoreBulkAction::make()
|
||||
->label(__('music_productions.restore')),
|
||||
ForceDeleteBulkAction::make()
|
||||
->label(__('music_productions.force_delete')),
|
||||
]),
|
||||
])
|
||||
->defaultSort('sort_order', 'asc');
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class PageResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
RelationManagers\TestimonialsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\RelationManagers;
|
||||
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class TestimonialsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'testimonials';
|
||||
|
||||
protected static ?string $title = 'Müşteri Görüşleri';
|
||||
|
||||
protected static ?string $modelLabel = 'Müşteri Görüşü';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Müşteri Görüşleri';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function canViewForRecord(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): bool
|
||||
{
|
||||
// Sadece Müşteri Görüşleri sayfası veya testimonials şablonu kullananlar için göster
|
||||
return $ownerRecord->slug === 'musteri-gorusleri' ||
|
||||
$ownerRecord->template === 'corporate.testimonials' ||
|
||||
$ownerRecord->is_homepage;
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Müşteri Adı')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('position')
|
||||
->label('Şirket / Ünvan')
|
||||
->maxLength(255),
|
||||
Textarea::make('content')
|
||||
->label('Görüş / Yorum')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
Select::make('rating')
|
||||
->label('Puan')
|
||||
->options([
|
||||
1 => '1 Yıldız',
|
||||
2 => '2 Yıldız',
|
||||
3 => '3 Yıldız',
|
||||
4 => '4 Yıldız',
|
||||
5 => '5 Yıldız',
|
||||
])
|
||||
->default(5)
|
||||
->required(),
|
||||
FileUpload::make('avatar')
|
||||
->label('Profil Resmi')
|
||||
->image()
|
||||
->directory('testimonials'),
|
||||
Toggle::make('is_active')
|
||||
->label('Aktif')
|
||||
->default(true),
|
||||
TextInput::make('sort_order')
|
||||
->label('Sıralama')
|
||||
->numeric()
|
||||
->default(0),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('Ad')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('position')
|
||||
->label('Ünvan')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('rating')
|
||||
->label('Puan')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->label('Aktif')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('sort_order')
|
||||
->label('Sıra')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make(),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionBuilderSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionTemplatesSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\TranslationsSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\LogosSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\PartnersSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\BankAccountsSection;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
@@ -49,6 +52,11 @@ class PageForm
|
||||
SectionTemplatesSection::make(),
|
||||
FooterTemplateSection::make(),
|
||||
|
||||
// Full Width - Logos (Only for Logolarımız template)
|
||||
LogosSection::make(),
|
||||
PartnersSection::make(),
|
||||
BankAccountsSection::make(),
|
||||
|
||||
// Full Width - Translations
|
||||
TranslationsSection::make(),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class BankAccountsSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('Banka Hesapları'))
|
||||
->description(__('Bu sayfada gösterilecek banka hesap bilgilerini buradan yönetebilirsiniz.'))
|
||||
->schema([
|
||||
Repeater::make('data.bank_accounts')
|
||||
->label('Hesaplar')
|
||||
->schema([
|
||||
TextInput::make('bank_name')
|
||||
->label('Banka Adı')
|
||||
->placeholder('Örn: Ziraat Bankası')
|
||||
->required(),
|
||||
|
||||
TextInput::make('branch_name')
|
||||
->label('Şube Adı / Kodu')
|
||||
->placeholder('Örn: Konak Şubesi / 1234'),
|
||||
|
||||
TextInput::make('account_holder')
|
||||
->label('Hesap Sahibi')
|
||||
->default('Trunçgil Teknoloji')
|
||||
->required(),
|
||||
|
||||
TextInput::make('iban')
|
||||
->label('IBAN')
|
||||
->placeholder('TR00 0000 0000 0000 0000 0000 00')
|
||||
->required(),
|
||||
|
||||
Select::make('currency')
|
||||
->label('Para Birimi')
|
||||
->options([
|
||||
'TRY' => 'Türk Lirası (₺)',
|
||||
'USD' => 'Amerikan Doları ($)',
|
||||
'EUR' => 'Euro (€)',
|
||||
'GBP' => 'Sterlin (£)',
|
||||
])
|
||||
->default('TRY')
|
||||
->live()
|
||||
->required(),
|
||||
|
||||
TextInput::make('swift_code')
|
||||
->label('SWIFT / BIC')
|
||||
->placeholder('Örn: TRHBTR2A')
|
||||
->required(fn (Get $get): bool => $get('currency') !== 'TRY'),
|
||||
|
||||
FileUpload::make('logo')
|
||||
->label('Banka Logosu')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('banks')
|
||||
->visibility('public'),
|
||||
])
|
||||
->grid(3)
|
||||
->columnSpanFull()
|
||||
->reorderableWithButtons()
|
||||
->collapsible()
|
||||
->cloneable(),
|
||||
])
|
||||
->visible(fn (Get $get): bool => $get('template') === 'corporate.bank-accounts')
|
||||
->columnSpanFull()
|
||||
->collapsible();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ class ContentSection
|
||||
|
||||
RichEditor::make('content')
|
||||
->label(__('pages.content_field'))
|
||||
->required()
|
||||
->fileAttachmentsDisk('public')
|
||||
->fileAttachmentsDirectory('pages')
|
||||
->fileAttachmentsVisibility('public')
|
||||
|
||||
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class FooterTemplateSection
|
||||
{
|
||||
public static function make(): Section
|
||||
@@ -17,9 +19,35 @@ class FooterTemplateSection
|
||||
return Section::make(__('pages.form_section_footer_template'))
|
||||
->description(__('pages.form_section_footer_template_desc'))
|
||||
->schema([
|
||||
Select::make('custom_footer_blade')
|
||||
->label('Custom Footer Blade')
|
||||
->options(function () {
|
||||
$files = File::glob(resource_path('views/partials/*.blade.php'));
|
||||
$options = [];
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["partials.{$name}"] = "Partial: {$name}";
|
||||
}
|
||||
|
||||
// Add other directories if needed
|
||||
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
|
||||
foreach ($componentFiles as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["components.front.{$name}"] = "Component: {$name}";
|
||||
}
|
||||
|
||||
return $options;
|
||||
})
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Select a physical Blade file to use as footer instead of a database template.')
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('footer_template_id')
|
||||
->label(__('pages.footer_template_field'))
|
||||
->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => empty($get('custom_footer_blade')))
|
||||
->searchable()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
|
||||
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class HeaderTemplateSection
|
||||
{
|
||||
public static function make(): Section
|
||||
@@ -17,9 +19,35 @@ class HeaderTemplateSection
|
||||
return Section::make(__('pages.form_section_header_template'))
|
||||
->description(__('pages.form_section_header_template_desc'))
|
||||
->schema([
|
||||
Select::make('custom_header_blade')
|
||||
->label('Custom Header Blade')
|
||||
->options(function () {
|
||||
$files = File::glob(resource_path('views/partials/*.blade.php'));
|
||||
$options = [];
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["partials.{$name}"] = "Partial: {$name}";
|
||||
}
|
||||
|
||||
// Add other directories if needed
|
||||
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
|
||||
foreach ($componentFiles as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["components.front.{$name}"] = "Component: {$name}";
|
||||
}
|
||||
|
||||
return $options;
|
||||
})
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Select a physical Blade file to use as header instead of a database template.')
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('header_template_id')
|
||||
->label(__('pages.header_template_field'))
|
||||
->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => empty($get('custom_header_blade')))
|
||||
->searchable()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class LogosSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('Logolar'))
|
||||
->description(__('Bu sayfada gösterilecek logoları buradan yönetebilirsiniz.'))
|
||||
->schema([
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
FileUpload::make('data.logo_1')
|
||||
->label(__('Logo 1'))
|
||||
->disk('public'),
|
||||
|
||||
FileUpload::make('data.logo_2')
|
||||
->label(__('Logo 2'))
|
||||
->disk('public'),
|
||||
|
||||
FileUpload::make('data.logo_3')
|
||||
->label(__('Logo 3'))
|
||||
->disk('public'),
|
||||
]),
|
||||
])
|
||||
->visible(fn (Get $get): bool => $get('template') === 'corporate.logos')
|
||||
->columnSpanFull()
|
||||
->collapsible();
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use App\Support\PageTemplateHero;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class PageSettingsSection
|
||||
{
|
||||
@@ -15,6 +17,13 @@ class PageSettingsSection
|
||||
{
|
||||
return Section::make(__('pages.form_section_page_settings'))
|
||||
->schema([
|
||||
Select::make('template')
|
||||
->label(__('pages.template_field'))
|
||||
->options(PageTemplateHero::templateFormOptions())
|
||||
->default('default')
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
FileUpload::make('featured_image')
|
||||
->label(__('pages.featured_image_field'))
|
||||
->image()
|
||||
@@ -27,7 +36,13 @@ class PageSettingsSection
|
||||
'4:3',
|
||||
'1:1',
|
||||
])
|
||||
->helperText(__('pages.featured_image_helper_text'))
|
||||
->helperText(function (Get $get): string {
|
||||
if (PageTemplateHero::hasHero($get('template'))) {
|
||||
return __('pages.featured_image_template_hero_helper');
|
||||
}
|
||||
|
||||
return __('pages.featured_image_helper_text');
|
||||
})
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('author_id')
|
||||
@@ -61,21 +76,10 @@ class PageSettingsSection
|
||||
->preload()
|
||||
->helperText(__('pages.parent_helper_text'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('template')
|
||||
->label(__('pages.template_field'))
|
||||
->options([
|
||||
'default' => __('pages.template_default'),
|
||||
'landing' => __('pages.template_landing'),
|
||||
'blog' => __('pages.template_blog'),
|
||||
'contact' => __('pages.template_contact'),
|
||||
])
|
||||
->default('default')
|
||||
->columnSpanFull(),
|
||||
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->label(__('pages.sort_order_field'))
|
||||
->numeric()
|
||||
->label(__('pages.sort_order_field'))
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText(__('pages.sort_order_helper_text'))
|
||||
->columnSpanFull(),
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class PartnersSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('Çözüm Ortakları'))
|
||||
->description(__('Bu sayfada gösterilecek çözüm ortaklarını buradan yönetebilirsiniz.'))
|
||||
->schema([
|
||||
Repeater::make('data.partners')
|
||||
->label('Partnerler')
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Partner Adı')
|
||||
->placeholder('Örn: Microsoft')
|
||||
->maxLength(120)
|
||||
->columnSpanFull(),
|
||||
|
||||
FileUpload::make('logo')
|
||||
->label('Logo')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('partners')
|
||||
->visibility('public')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('url')
|
||||
->label('Website URL')
|
||||
->url()
|
||||
->placeholder('https://example.com')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
|
||||
->grid(4)
|
||||
->columnSpanFull()
|
||||
->reorderableWithButtons()
|
||||
->collapsible()
|
||||
->cloneable(),
|
||||
])
|
||||
->visible(fn (Get $get): bool => $get('template') === 'corporate.partners')
|
||||
->columnSpanFull()
|
||||
->collapsible();
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ class SectionBuilderSection
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('pages/sections')
|
||||
->imageEditor()
|
||||
->directory('pages/sections')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(function ($component, $state, Get $get) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateProductCategory extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProductCategoryResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditProductCategory extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProductCategoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProductCategories extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProductCategoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories;
|
||||
|
||||
use App\Filament\Admin\Resources\ProductCategories\Pages\CreateProductCategory;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Pages\EditProductCategory;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Pages\ListProductCategories;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Schemas\ProductCategoryForm;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Tables\ProductCategoriesTable;
|
||||
use App\Models\ProductCategory;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProductCategoryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ProductCategory::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('product_categories.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('product_categories.menu_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('product_categories.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('product_categories.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ProductCategoryForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ProductCategoriesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListProductCategories::route('/'),
|
||||
'create' => CreateProductCategory::route('/create'),
|
||||
'edit' => EditProductCategory::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Models\ProductCategory;
|
||||
|
||||
class ProductCategoryForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Section::make(__('product_categories.general_section'))
|
||||
->schema([
|
||||
TextInput::make('slug')
|
||||
->label(__('product_categories.slug'))
|
||||
->required()
|
||||
->unique(ignoreRecord: true),
|
||||
Select::make('parent_id')
|
||||
->label(__('product_categories.parent_category'))
|
||||
->options(function () {
|
||||
return ProductCategory::all()->mapWithKeys(function ($category) {
|
||||
$title = is_array($category->title)
|
||||
? ($category->title[app()->getLocale()] ?? \Illuminate\Support\Arr::first($category->title))
|
||||
: $category->title;
|
||||
return [$category->id => $title ?? ('Category #' . $category->id)];
|
||||
});
|
||||
})
|
||||
->searchable(),
|
||||
TextInput::make('sort_order')
|
||||
->label(__('product_categories.sort_order'))
|
||||
->numeric()
|
||||
->default(0),
|
||||
Toggle::make('is_active')
|
||||
->label(__('product_categories.is_active'))
|
||||
->default(true),
|
||||
])->columns(2),
|
||||
|
||||
Section::make(__('product_categories.translations_section'))
|
||||
->schema([
|
||||
TranslationTabs::make([
|
||||
'title' => [
|
||||
'type' => 'text',
|
||||
'label' => __('product_categories.title'),
|
||||
'required' => true,
|
||||
],
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Tables;
|
||||
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
|
||||
class ProductCategoriesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label(__('product_categories.title'))
|
||||
->getStateUsing(fn ($record) => $record->translate('title'))
|
||||
->searchable(query: function ($query, $search) {
|
||||
return $query->whereHas('translations', function ($q) use ($search) {
|
||||
$q->where('field_name', 'title')
|
||||
->where('field_value', 'like', "%{$search}%")
|
||||
->where('language_code', app()->getLocale());
|
||||
});
|
||||
}),
|
||||
TextColumn::make('slug')
|
||||
->label(__('product_categories.slug'))
|
||||
->searchable(),
|
||||
TextColumn::make('parent.title')
|
||||
->getStateUsing(fn ($record) => $record->parent?->translate('title'))
|
||||
->label(__('product_categories.parent')),
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('product_categories.sort_order'))
|
||||
->sortable(),
|
||||
ToggleColumn::make('is_active')
|
||||
->label(__('product_categories.is_active')),
|
||||
TextColumn::make('created_at')
|
||||
->label(__('product_categories.created_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\Products\ProductResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateProduct extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProductResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\Products\ProductResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditProduct extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Products\ProductResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProducts extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products;
|
||||
|
||||
use App\Filament\Admin\Resources\Products\Pages\CreateProduct;
|
||||
use App\Filament\Admin\Resources\Products\Pages\EditProduct;
|
||||
use App\Filament\Admin\Resources\Products\Pages\ListProducts;
|
||||
use App\Filament\Admin\Resources\Products\Schemas\ProductForm;
|
||||
use App\Filament\Admin\Resources\Products\Tables\ProductsTable;
|
||||
use App\Models\Product;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProductResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Product::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCube;
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('products.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('products.menu_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('products.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('products.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ProductForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ProductsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListProducts::route('/'),
|
||||
'create' => CreateProduct::route('/create'),
|
||||
'edit' => EditProduct::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Schemas;
|
||||
|
||||
use App\Models\Language;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class LandingPageSection
|
||||
{
|
||||
private static function getIconOptions(): array
|
||||
{
|
||||
$icons = [
|
||||
'cloud-network-2', 'touchscreen', 'lock', 'rocket', 'bar-chart', 'safe',
|
||||
'controls', 'checked', 'calendar', 'currency', 'server', 'devices',
|
||||
'search', 'compare', 'smartphone', 'headphone', 'secure', 'verified',
|
||||
'wallet', 'shopping-cart', 'e-commerce', 'code', 'bulb', 'gears',
|
||||
'shield', 'star', 'check-circle', 'bolt', 'zap', 'heart', 'gift',
|
||||
];
|
||||
|
||||
return array_combine($icons, array_map(fn($icon) => ucwords(str_replace('-', ' ', $icon)), $icons));
|
||||
}
|
||||
|
||||
private static function getBackgroundOptions(): array
|
||||
{
|
||||
$backgrounds = [
|
||||
// Abstract backgrounds
|
||||
'assets/img/photos/bg2.jpg' => 'Background 2 (Abstract)',
|
||||
'assets/img/photos/bg3.jpg' => 'Background 3 (Abstract)',
|
||||
'assets/img/photos/bg4.jpg' => 'Background 4 (Abstract)',
|
||||
'assets/img/photos/bg13.jpg' => 'Background 13 (Abstract)',
|
||||
// Gradient backgrounds
|
||||
'assets/img/photos/bg14.png' => 'Background 14 (Gradient)',
|
||||
'assets/img/photos/bg15.png' => 'Background 15 (Gradient)',
|
||||
'assets/img/photos/bg16.png' => 'Background 16 (Gradient)',
|
||||
'assets/img/photos/bg17.png' => 'Background 17 (Gradient)',
|
||||
'assets/img/photos/bg18.png' => 'Background 18 (Gradient)',
|
||||
'assets/img/photos/bg19.png' => 'Background 19 (Gradient)',
|
||||
'assets/img/photos/bg20.png' => 'Background 20 (Gradient)',
|
||||
'assets/img/photos/bg21.png' => 'Background 21 (Gradient)',
|
||||
'assets/img/photos/bg22.png' => 'Background 22 (Gradient)',
|
||||
'assets/img/photos/bg23.png' => 'Background 23 (Gradient)',
|
||||
'assets/img/photos/bg24.png' => 'Background 24 (Gradient)',
|
||||
'assets/img/photos/bg25.png' => 'Background 25 (Gradient)',
|
||||
];
|
||||
|
||||
return $backgrounds;
|
||||
}
|
||||
|
||||
public static function make(): Section
|
||||
{
|
||||
$languages = Language::active()->ordered()->get();
|
||||
$iconOptions = self::getIconOptions();
|
||||
$backgroundOptions = self::getBackgroundOptions();
|
||||
|
||||
return Section::make(__('products.landing_page_section'))
|
||||
->schema([
|
||||
// Hero Section
|
||||
Section::make(__('products.hero_section'))
|
||||
->schema([
|
||||
TextInput::make('landing_page_data.hero.slogan')
|
||||
->label(__('products.hero_slogan'))
|
||||
->columnSpanFull(),
|
||||
Textarea::make('landing_page_data.hero.sub_slogan')
|
||||
->label(__('products.hero_sub_slogan'))
|
||||
->rows(2)
|
||||
->columnSpanFull(),
|
||||
Select::make('landing_page_data.hero.background_image')
|
||||
->label(__('products.hero_background_image'))
|
||||
->options($backgroundOptions)
|
||||
->searchable()
|
||||
->placeholder(__('products.select_background'))
|
||||
->helperText(__('products.hero_background_image_helper'))
|
||||
->columnSpanFull(),
|
||||
TextInput::make('landing_page_data.hero.app_store_link')
|
||||
->label(__('products.app_store_link'))
|
||||
->url()
|
||||
->placeholder('https://apps.apple.com/...'),
|
||||
TextInput::make('landing_page_data.hero.google_play_link')
|
||||
->label(__('products.google_play_link'))
|
||||
->url()
|
||||
->placeholder('https://play.google.com/...'),
|
||||
FileUpload::make('landing_page_data.hero.featured_image')
|
||||
->label(__('products.hero_featured_image'))
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('products/landing')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(2)
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
|
||||
// App Features Section
|
||||
Section::make(__('products.app_features_section'))
|
||||
->schema([
|
||||
Repeater::make('landing_page_data.features')
|
||||
->label(__('products.features'))
|
||||
->schema([
|
||||
TextInput::make('icon')
|
||||
->label(__('products.feature_icon')),
|
||||
TextInput::make('title')
|
||||
->label(__('products.feature_title'))
|
||||
->columnSpanFull(),
|
||||
Textarea::make('description')
|
||||
->label(__('products.feature_description'))
|
||||
->rows(2)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(1)
|
||||
->defaultItems(0)
|
||||
->addActionLabel(__('products.add_feature'))
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): ?string => $state['title'] ?? __('products.feature') . ' #' . ($state['_index'] ?? ''))
|
||||
->columnSpanFull(),
|
||||
|
||||
// Note: Translations should match the order of features above
|
||||
// Each feature translation array index should correspond to the feature index
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
|
||||
// How It Works Section
|
||||
Section::make(__('products.how_it_works_section'))
|
||||
->schema([
|
||||
FileUpload::make('landing_page_data.how_it_works.download_image')
|
||||
->label(__('products.how_it_works_image'))
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('products/landing')
|
||||
->columnSpanFull(),
|
||||
TextInput::make('landing_page_data.how_it_works.download_form_label')
|
||||
->label(__('products.download_form_label'))
|
||||
->placeholder(__('products.download_form_label_placeholder'))
|
||||
->columnSpanFull(),
|
||||
|
||||
// Steps (4 steps with translations)
|
||||
Repeater::make('landing_page_data.how_it_works.steps')
|
||||
->label(__('products.steps'))
|
||||
->schema([
|
||||
TextInput::make('number')
|
||||
->label(__('products.step_number'))
|
||||
->numeric()
|
||||
->default(fn ($get) => ($get('../../../_index') ?? 0) + 1),
|
||||
TextInput::make('title')
|
||||
->label(__('products.step_title'))
|
||||
->columnSpanFull(),
|
||||
Textarea::make('description')
|
||||
->label(__('products.step_description'))
|
||||
->rows(2)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(1)
|
||||
->defaultItems(0)
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): ?string => __('products.step') . ' ' . ($state['number'] ?? '') . ': ' . ($state['title'] ?? ''))
|
||||
->columnSpanFull(),
|
||||
|
||||
// Note: Steps translations should match the order of steps above
|
||||
// Each step translation array index should correspond to the step index
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
|
||||
// Video Section
|
||||
Section::make(__('products.video_section'))
|
||||
->schema([
|
||||
TextInput::make('landing_page_data.video.youtube_video_id')
|
||||
->label(__('products.youtube_video_id'))
|
||||
->placeholder('165101721')
|
||||
->helperText(__('products.youtube_video_id_helper'))
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
|
||||
// FAQ Section
|
||||
Section::make(__('products.faq_section'))
|
||||
->schema([
|
||||
Repeater::make('landing_page_data.faqs')
|
||||
->label(__('products.faqs'))
|
||||
->schema([
|
||||
TextInput::make('question')
|
||||
->label(__('products.faq_question'))
|
||||
->columnSpanFull(),
|
||||
Textarea::make('answer')
|
||||
->label(__('products.faq_answer'))
|
||||
->rows(3)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(1)
|
||||
->defaultItems(0)
|
||||
->addActionLabel(__('products.add_faq'))
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): ?string => $state['question'] ?? __('products.faq') . ' #' . ($state['_index'] ?? ''))
|
||||
->columnSpanFull(),
|
||||
|
||||
// Note: FAQ translations should match the order of FAQs above
|
||||
// Each FAQ translation array index should correspond to the FAQ index
|
||||
])
|
||||
->collapsible()
|
||||
->collapsed(),
|
||||
])
|
||||
->visible(fn (Get $get) => $get('type') === 'product')
|
||||
->columnSpanFull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\Products\Schemas\LandingPageSection;
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Support\Str;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
class ProductForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Section::make(__('products.general_section'))
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label(__('products.title'))
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (Get $get, Set $set, ?string $state) {
|
||||
$set('slug', Str::slug($state));
|
||||
$set('translations.' . app()->getLocale() . '.title', $state);
|
||||
})
|
||||
->formatStateUsing(fn ($record) => $record?->translate('title'))
|
||||
->dehydrated(false),
|
||||
TextInput::make('slug')
|
||||
->label(__('products.slug'))
|
||||
->unique(ignoreRecord: true),
|
||||
Select::make('type')
|
||||
->label(__('products.type'))
|
||||
->options([
|
||||
'product' => __('products.product'),
|
||||
'service' => __('products.service'),
|
||||
]),
|
||||
Select::make('product_category_id')
|
||||
->label(__('products.category'))
|
||||
->options(function () {
|
||||
return ProductCategory::all()->mapWithKeys(function ($category) {
|
||||
return [$category->id => $category->translate('title') ?? ('Category #' . $category->id)];
|
||||
});
|
||||
})
|
||||
->searchable(),
|
||||
FileUpload::make('hero_image')
|
||||
->label(__('products.hero_image'))
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('products'),
|
||||
TextInput::make('view_template')
|
||||
->label(__('products.view_template'))
|
||||
->placeholder(__('products.view_template_placeholder'))
|
||||
->helperText(__('products.view_template_helper')),
|
||||
TextInput::make('external_url')
|
||||
->label(__('products.external_url'))
|
||||
->url()
|
||||
->helperText(__('products.external_url_helper')),
|
||||
TextInput::make('sort_order')
|
||||
->label(__('products.sort_order'))
|
||||
->numeric()
|
||||
->default(0),
|
||||
Toggle::make('is_active')
|
||||
->label(__('products.is_active'))
|
||||
->default(true),
|
||||
])->columns(2),
|
||||
|
||||
Section::make(__('products.translations_section'))
|
||||
->schema([
|
||||
TranslationTabs::make([
|
||||
'title' => [
|
||||
'type' => 'text',
|
||||
'label' => __('products.title'),
|
||||
'required' => false,
|
||||
],
|
||||
'content' => [
|
||||
'type' => 'richtext',
|
||||
'label' => __('products.content'),
|
||||
],
|
||||
]),
|
||||
]),
|
||||
|
||||
// Landing Page Section (only for products)
|
||||
LandingPageSection::make(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Tables;
|
||||
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
|
||||
class ProductsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
ImageColumn::make('hero_image')
|
||||
->label(__('products.hero_image'))
|
||||
->disk('public'),
|
||||
TextColumn::make('title')
|
||||
->label(__('products.title'))
|
||||
->getStateUsing(fn ($record) => $record->translate('title'))
|
||||
->searchable(query: function ($query, $search) {
|
||||
return $query->whereHas('translations', function ($q) use ($search) {
|
||||
$q->where('field_name', 'title')
|
||||
->where('field_value', 'like', "%{$search}%")
|
||||
->where('language_code', app()->getLocale());
|
||||
});
|
||||
}),
|
||||
TextColumn::make('type')
|
||||
->label(__('products.type'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'product' => 'info',
|
||||
'service' => 'success',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'product' => __('products.product'),
|
||||
'service' => __('products.service'),
|
||||
default => $state,
|
||||
}),
|
||||
TextColumn::make('category.title')
|
||||
->getStateUsing(fn ($record) => $record->category?->translate('title'))
|
||||
->label(__('products.category')),
|
||||
ToggleColumn::make('is_active')
|
||||
->label(__('products.is_active')),
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('products.sort_order'))
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Proposals\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class CreateProposal extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProposalResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('proposal.create');
|
||||
}
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Proposals\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EditProposal extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProposalResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('proposal.edit');
|
||||
}
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
// Teklif linkini tarayıcı panosuna kopyala
|
||||
Action::make('copy_link')
|
||||
->label(__('proposal.copy_link'))
|
||||
->icon('heroicon-o-clipboard-document-check')
|
||||
->color('info')
|
||||
->action(function () {
|
||||
$url = route('proposals.show', $this->record->slug);
|
||||
|
||||
// Livewire v3: sunucu tarafında JS enjekte et (user-gesture gerektirmez)
|
||||
$escaped = addslashes($url);
|
||||
$this->js("(function(){var t=document.createElement('textarea');t.value='{$escaped}';t.style.position='fixed';t.style.opacity='0';document.body.appendChild(t);t.focus();t.select();try{document.execCommand('copy')}catch(e){}document.body.removeChild(t);})();");
|
||||
|
||||
Notification::make()
|
||||
->title(__('proposal.link_copied'))
|
||||
->body($url)
|
||||
->success()
|
||||
->duration(6000)
|
||||
->send();
|
||||
})
|
||||
->visible(fn () => $this->record !== null),
|
||||
|
||||
// Yeni rastgele benzersiz slug üret ve kaydet
|
||||
Action::make('regenerate_slug')
|
||||
->label(__('proposal.regenerate_slug'))
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('warning')
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('proposal.regenerate_slug_confirm_heading'))
|
||||
->modalDescription(__('proposal.regenerate_slug_confirm_body'))
|
||||
->modalSubmitActionLabel(__('proposal.regenerate_slug_confirm_button'))
|
||||
->action(function () {
|
||||
$newSlug = 'teklif-' . strtolower(Str::random(4)) . '-' . date('ymd') . '-' . strtolower(Str::random(4));
|
||||
|
||||
// Benzersizliği garanti et
|
||||
while (\App\Models\Proposal::where('slug', $newSlug)->where('id', '!=', $this->record->id)->exists()) {
|
||||
$newSlug = 'teklif-' . strtolower(Str::random(4)) . '-' . date('ymd') . '-' . strtolower(Str::random(4));
|
||||
}
|
||||
|
||||
$this->record->update(['slug' => $newSlug]);
|
||||
|
||||
$newUrl = route('proposals.show', $newSlug);
|
||||
|
||||
Notification::make()
|
||||
->title(__('proposal.regenerate_slug_success'))
|
||||
->body($newUrl)
|
||||
->success()
|
||||
->duration(8000)
|
||||
->send();
|
||||
|
||||
// Formu yenile
|
||||
$this->fillForm();
|
||||
})
|
||||
->visible(fn () => $this->record !== null),
|
||||
|
||||
// Teklifi yeni sekmede önizle
|
||||
Action::make('preview')
|
||||
->label(__('proposal.preview'))
|
||||
->icon('heroicon-o-eye')
|
||||
->color('gray')
|
||||
->url(fn () => $this->record ? route('proposals.show', $this->record->slug) : '#')
|
||||
->openUrlInNewTab()
|
||||
->visible(fn () => $this->record !== null),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Proposals\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Proposals\ProposalResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProposals extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProposalResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('proposal.title');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make()
|
||||
->label(__('proposal.create')),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Proposals;
|
||||
|
||||
use App\Filament\Admin\Resources\Proposals\Pages\CreateProposal;
|
||||
use App\Filament\Admin\Resources\Proposals\Pages\EditProposal;
|
||||
use App\Filament\Admin\Resources\Proposals\Pages\ListProposals;
|
||||
use App\Filament\Admin\Resources\Proposals\Schemas\ProposalForm;
|
||||
use App\Filament\Admin\Resources\Proposals\Tables\ProposalsTable;
|
||||
use App\Models\Proposal;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class ProposalResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Proposal::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('proposal.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('proposal.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('proposal.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ProposalForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ProposalsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListProposals::route('/'),
|
||||
'create' => CreateProposal::route('/create'),
|
||||
'edit' => EditProposal::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getRecordRouteBindingEloquentQuery(): Builder
|
||||
{
|
||||
return parent::getRecordRouteBindingEloquentQuery()
|
||||
->withoutGlobalScopes([
|
||||
SoftDeletingScope::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Proposals\Schemas;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Schemas\Components\Tabs;
|
||||
use Filament\Schemas\Components\Tabs\Tab;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\CodeEditor;
|
||||
use Filament\Forms\Components\CodeEditor\Enums\Language as CodeEditorLanguage;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class ProposalForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Tabs::make('Tabs')
|
||||
->tabs([
|
||||
Tab::make('Teklif Detayları ve İçerik')
|
||||
->icon('heroicon-m-document-text')
|
||||
->schema([
|
||||
// Section 2: side-by-side Markdown editor (CodeMirror) + live preview
|
||||
Section::make(__('proposal.content_section'))
|
||||
->description('Sol tarafta Markdown formatında içeriği düzenleyin, sağ tarafta canlı olarak nasıl görüneceğini izleyin.')
|
||||
->columns(2)
|
||||
->schema([
|
||||
CodeEditor::make('content')
|
||||
->label(__('proposal.content_field'))
|
||||
->hiddenLabel()
|
||||
->required()
|
||||
->language(CodeEditorLanguage::Markdown)
|
||||
->live(onBlur: false)
|
||||
->columnSpan(1)
|
||||
->extraAttributes([
|
||||
'style' => 'min-height: 700px; max-height: 700px; overflow-y: auto;',
|
||||
'data-field-name' => 'content',
|
||||
]),
|
||||
|
||||
View::make('filament.components.markdown-preview')
|
||||
->columnSpan(1),
|
||||
]),
|
||||
]),
|
||||
Tab::make('Durum ve Ayarlar')
|
||||
->icon('heroicon-m-cog-6-tooth')
|
||||
->schema([
|
||||
// Section 1: compact top metadata and configuration
|
||||
Section::make(__('proposal.settings_section'))
|
||||
->columns(12)
|
||||
->schema([
|
||||
TextInput::make('title')
|
||||
->label(__('proposal.title_field'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(function (string $operation, $state, callable $set) {
|
||||
if ($operation !== 'create') {
|
||||
return;
|
||||
}
|
||||
$set('slug', \Str::slug($state));
|
||||
})
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('slug')
|
||||
->label(__('proposal.slug_field'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->unique(ignoreRecord: true)
|
||||
->rules(['alpha_dash'])
|
||||
->helperText(__('proposal.slug_helper'))
|
||||
->columnSpan(4),
|
||||
|
||||
Select::make('status')
|
||||
->label(__('proposal.status_field'))
|
||||
->options([
|
||||
'draft' => __('proposal.status_draft'),
|
||||
'sent' => __('proposal.status_sent'),
|
||||
'accepted' => __('proposal.status_accepted'),
|
||||
'rejected' => __('proposal.status_rejected'),
|
||||
'revised' => __('proposal.status_revised'),
|
||||
])
|
||||
->default('draft')
|
||||
->required()
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('client_name')
|
||||
->label(__('proposal.client_name_field'))
|
||||
->required()
|
||||
->maxLength(255)
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('client_email')
|
||||
->label(__('proposal.client_email_field'))
|
||||
->email()
|
||||
->maxLength(255)
|
||||
->columnSpan(4),
|
||||
|
||||
DatePicker::make('valid_until')
|
||||
->label(__('proposal.valid_until_field'))
|
||||
->native(false)
|
||||
->displayFormat('d.m.Y')
|
||||
->columnSpan(4),
|
||||
|
||||
TextInput::make('total_price')
|
||||
->label(__('proposal.total_price_field'))
|
||||
->numeric()
|
||||
->prefix(fn ($get) => match ($get('currency')) {
|
||||
'USD' => '$',
|
||||
'EUR' => '€',
|
||||
default => '₺',
|
||||
})
|
||||
->columnSpan(3),
|
||||
|
||||
Select::make('currency')
|
||||
->label(__('proposal.currency_field'))
|
||||
->options([
|
||||
'TRY' => 'TL (₺)',
|
||||
'USD' => 'USD ($)',
|
||||
'EUR' => 'EUR (€)',
|
||||
])
|
||||
->default('TRY')
|
||||
->required()
|
||||
->live()
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('meta.accent_color')
|
||||
->label(__('proposal.accent_color_field'))
|
||||
->options([
|
||||
'indigo' => 'Premium Indigo (Mor/Mavi)',
|
||||
'emerald' => 'Emerald Medical (Zümrüt Yeşil)',
|
||||
'cyberpunk' => 'Cyberpunk Neon (Pembe/Mavi)',
|
||||
'coral' => 'Coral Corporate (Mercan Kırmızı)',
|
||||
'amber' => 'Amber Executive (Kehribar/Altın)',
|
||||
])
|
||||
->default('indigo')
|
||||
->live()
|
||||
->columnSpan(4),
|
||||
|
||||
Checkbox::make('meta.show_calculator')
|
||||
->label(__('proposal.show_calculator_field'))
|
||||
->default(true)
|
||||
->columnSpan(4),
|
||||
|
||||
// Client feedback inside this section — only visible when present
|
||||
Textarea::make('client_feedback')
|
||||
->label('Müşteri Geri Bildirimi / Revizyon Notu')
|
||||
->rows(3)
|
||||
->disabled()
|
||||
->dehydrated(false)
|
||||
->placeholder('Henüz geri bildirim bırakılmadı.')
|
||||
->visible(fn ($record) => $record !== null && !empty($record->client_feedback))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]),
|
||||
])->columnSpanFull()
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Proposals\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProposalsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label(__('proposal.table_title'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(40),
|
||||
|
||||
TextColumn::make('client_name')
|
||||
->label(__('proposal.table_client'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(30),
|
||||
|
||||
TextColumn::make('total_price')
|
||||
->label(__('proposal.table_price'))
|
||||
->money(fn ($record) => $record->currency)
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label(__('proposal.table_status'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'draft' => 'gray',
|
||||
'sent' => 'info',
|
||||
'accepted' => 'success',
|
||||
'rejected' => 'danger',
|
||||
'revised' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => match ($state) {
|
||||
'draft' => __('proposal.status_draft'),
|
||||
'sent' => __('proposal.status_sent'),
|
||||
'accepted' => __('proposal.status_accepted'),
|
||||
'rejected' => __('proposal.status_rejected'),
|
||||
'revised' => __('proposal.status_revised'),
|
||||
default => $state,
|
||||
}),
|
||||
|
||||
TextColumn::make('slug')
|
||||
->label(__('proposal.table_slug'))
|
||||
->searchable()
|
||||
->copyable()
|
||||
->copyMessage(__('proposal.link_copied'))
|
||||
->icon('heroicon-o-document-duplicate')
|
||||
->limit(25),
|
||||
|
||||
TextColumn::make('valid_until')
|
||||
->label(__('proposal.table_valid_until'))
|
||||
->date('d.m.Y')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('views_count')
|
||||
->label(__('proposal.table_views'))
|
||||
->sortable()
|
||||
->alignCenter(),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->label(__('proposal.table_created_at'))
|
||||
->dateTime('d.m.Y H:i')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('status')
|
||||
->label(__('proposal.status_field'))
|
||||
->options([
|
||||
'draft' => __('proposal.status_draft'),
|
||||
'sent' => __('proposal.status_sent'),
|
||||
'accepted' => __('proposal.status_accepted'),
|
||||
'rejected' => __('proposal.status_rejected'),
|
||||
'revised' => __('proposal.status_revised'),
|
||||
]),
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->label(__('proposal.edit')),
|
||||
])
|
||||
->actions([
|
||||
Action::make('view_proposal')
|
||||
->label(__('proposal.preview'))
|
||||
->icon('heroicon-o-eye')
|
||||
->url(fn ($record) => route('proposals.show', $record->slug))
|
||||
->openUrlInNewTab(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->label(__('proposal.delete')),
|
||||
]),
|
||||
])
|
||||
->defaultSort('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,30 @@ class EditSetting extends EditRecord
|
||||
return __('settings.edit');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
$record = $this->getRecord();
|
||||
|
||||
if (isset($data['type'])) {
|
||||
$type = $data['type'];
|
||||
|
||||
// Map the setting type to the form field name
|
||||
$virtualField = match ($type) {
|
||||
'string', 'text', 'json', 'integer', 'float' => 'value_text',
|
||||
default => 'value_' . $type,
|
||||
};
|
||||
|
||||
try {
|
||||
// Model accessor'ını kullanarak değeri al
|
||||
$data[$virtualField] = $record->{$virtualField};
|
||||
} catch (\Exception $e) {
|
||||
// Accessor bulunamazsa devam et
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -42,6 +42,7 @@ class SettingForm
|
||||
->regex('/^[a-z0-9_-]+$/')
|
||||
->maxLength(255)
|
||||
->disabled(fn ($record) => $record !== null)
|
||||
->live(onBlur: true)
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('label')
|
||||
@@ -114,10 +115,28 @@ class SettingForm
|
||||
->label(__('settings.value'))
|
||||
->helperText(__('settings.value_helper'))
|
||||
->required()
|
||||
->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json']))
|
||||
->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json', 'integer', 'float']) && !in_array($get('key'), ['default_header', 'default_footer']))
|
||||
->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3)
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('value_header_template')
|
||||
->label('Default Header Template')
|
||||
->options(\App\Models\HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => $get('key') === 'default_header')
|
||||
->statePath('value')
|
||||
->searchable()
|
||||
->preload()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('value_footer_template')
|
||||
->label('Default Footer Template')
|
||||
->options(\App\Models\FooterTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => $get('key') === 'default_footer')
|
||||
->statePath('value')
|
||||
->searchable()
|
||||
->preload()
|
||||
->columnSpanFull(),
|
||||
|
||||
Toggle::make('value_boolean')
|
||||
->label(__('settings.value'))
|
||||
->helperText(__('settings.value_helper'))
|
||||
@@ -134,7 +153,15 @@ class SettingForm
|
||||
->directory('settings')
|
||||
->acceptedFileTypes(['image/*', 'application/pdf', 'text/*'])
|
||||
->maxSize(10240) // 10MB
|
||||
->columnSpanFull(),
|
||||
->openable()
|
||||
->downloadable()
|
||||
->previewable(true)
|
||||
->columnSpanFull()
|
||||
->afterStateHydrated(function ($component, $state, $record) {
|
||||
if ($record && $record->type === 'file') {
|
||||
$component->state($record->value);
|
||||
}
|
||||
}),
|
||||
|
||||
DatePicker::make('value_date')
|
||||
->label(__('settings.value'))
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateSiteTranslation extends CreateRecord
|
||||
{
|
||||
protected static string $resource = SiteTranslationResource::class;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditSiteTranslation extends EditRecord
|
||||
{
|
||||
protected static string $resource = SiteTranslationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class ListSiteTranslations extends ListRecords
|
||||
{
|
||||
protected static string $resource = SiteTranslationResource::class;
|
||||
|
||||
public bool $useDataGrid = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
parent::mount();
|
||||
|
||||
// Session'dan görünüm tercihini al, yoksa varsayılan olarak false (eski görünüm)
|
||||
$this->useDataGrid = session('site_translations_use_datagrid', false);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$useDataGrid = session('site_translations_use_datagrid', false);
|
||||
|
||||
return [
|
||||
Action::make('toggleView')
|
||||
->label($useDataGrid ? 'Tablo Görünümü' : 'DataGrid Görünümü')
|
||||
->icon($useDataGrid ? 'heroicon-o-table-cells' : 'heroicon-o-squares-2x2')
|
||||
->color('gray')
|
||||
->action(function () {
|
||||
$currentValue = session('site_translations_use_datagrid', false);
|
||||
$newValue = !$currentValue;
|
||||
session(['site_translations_use_datagrid' => $newValue]);
|
||||
session()->save(); // Session'ı hemen kaydet
|
||||
|
||||
// Sayfayı yenile
|
||||
$this->redirect($this->getResource()::getUrl('index'));
|
||||
}),
|
||||
CreateAction::make()
|
||||
->visible(!$useDataGrid), // Sadece eski görünümde göster
|
||||
];
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$useDataGrid = session('site_translations_use_datagrid', false);
|
||||
|
||||
if ($useDataGrid) {
|
||||
return view('filament.admin.resources.site-translations.datagrid')
|
||||
->layout($this->getLayout());
|
||||
}
|
||||
|
||||
return parent::render();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Schemas;
|
||||
|
||||
use App\Models\Language;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class SiteTranslationForm
|
||||
{
|
||||
public static function make(Schema $schema): Schema
|
||||
{
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
|
||||
$translationFields = [];
|
||||
foreach ($languages as $language) {
|
||||
$translationFields[] = Textarea::make($language->code)
|
||||
->label($language->name . " ({$language->code})")
|
||||
->rows(2);
|
||||
}
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
Section::make()
|
||||
->schema([
|
||||
Textarea::make('key')
|
||||
->label('Key / Original Text')
|
||||
->required()
|
||||
->columnSpanFull()
|
||||
->helperText('The original text to be translated. Use this exact text in t() helper.'),
|
||||
|
||||
Group::make()
|
||||
->schema($translationFields)
|
||||
->statePath('translations')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
use App\Filament\Admin\Resources\SiteTranslations\Schemas\SiteTranslationForm;
|
||||
use App\Filament\Admin\Resources\SiteTranslations\Tables\SiteTranslationsTable;
|
||||
use App\Models\SiteTranslation;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
use UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
class SiteTranslationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = SiteTranslation::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-language';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?int $navigationSort = 100;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return SiteTranslationForm::make($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return SiteTranslationsTable::make($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListSiteTranslations::route('/'),
|
||||
'create' => Pages\CreateSiteTranslation::route('/create'),
|
||||
'edit' => Pages\EditSiteTranslation::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('Site Translations');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('Translation');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('Translations');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Tables;
|
||||
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use App\Models\Language;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Actions\ImportAction;
|
||||
use Filament\Actions\ExportAction;
|
||||
use App\Filament\Imports\SiteTranslationImporter;
|
||||
use App\Filament\Exports\SiteTranslationExporter;
|
||||
|
||||
class SiteTranslationsTable
|
||||
{
|
||||
public static function make(Table $table): Table
|
||||
{
|
||||
$defaultLocale = app()->getLocale();
|
||||
|
||||
return $table
|
||||
->headerActions([
|
||||
ImportAction::make()
|
||||
->importer(SiteTranslationImporter::class),
|
||||
ExportAction::make()
|
||||
->exporter(SiteTranslationExporter::class)
|
||||
])
|
||||
->columns([
|
||||
TextColumn::make('key')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(50)
|
||||
->wrap(),
|
||||
|
||||
TextColumn::make("translations.{$defaultLocale}")
|
||||
->label("Translation ({$defaultLocale})")
|
||||
->searchable()
|
||||
->limit(50)
|
||||
->wrap()
|
||||
->placeholder('No translation'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->modalWidth('xl')
|
||||
->form(function () {
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
|
||||
$translationFields = [];
|
||||
foreach ($languages as $language) {
|
||||
$translationFields[] = Textarea::make($language->code)
|
||||
->label($language->name . " ({$language->code})")
|
||||
->rows(2);
|
||||
}
|
||||
|
||||
return [
|
||||
Section::make()
|
||||
->schema([
|
||||
Textarea::make('key')
|
||||
->label('Key / Original Text')
|
||||
->required()
|
||||
->columnSpanFull()
|
||||
->helperText('The original text to be translated. Use this exact text in t() helper.'),
|
||||
|
||||
Group::make()
|
||||
->schema($translationFields)
|
||||
->statePath('translations')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
];
|
||||
}),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Exports;
|
||||
|
||||
use App\Models\SiteTranslation;
|
||||
use App\Models\Language;
|
||||
use Filament\Actions\Exports\ExportColumn;
|
||||
use Filament\Actions\Exports\Exporter;
|
||||
use Filament\Actions\Exports\Models\Export;
|
||||
|
||||
class SiteTranslationExporter extends Exporter
|
||||
{
|
||||
protected static ?string $model = SiteTranslation::class;
|
||||
|
||||
public static function getColumns(): array
|
||||
{
|
||||
$columns = [
|
||||
ExportColumn::make('key')->label('Key'),
|
||||
];
|
||||
|
||||
// Veritabanı bağlantısı yoksa veya tablo yoksa hata vermemesi için try-catch veya kontrol
|
||||
try {
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
foreach ($languages as $language) {
|
||||
$columns[] = ExportColumn::make("translations.{$language->code}")
|
||||
->label($language->name . " ({$language->code})");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Migration henüz çalışmadıysa
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public static function getCompletedNotificationBody(Export $export): string
|
||||
{
|
||||
$body = 'Your site translation export has completed and ' . number_format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.';
|
||||
|
||||
if ($failedRowsCount = $export->getFailedRowsCount()) {
|
||||
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Imports;
|
||||
|
||||
use App\Models\SiteTranslation;
|
||||
use App\Models\Language;
|
||||
use Filament\Actions\Imports\ImportColumn;
|
||||
use Filament\Actions\Imports\Importer;
|
||||
use Filament\Actions\Imports\Models\Import;
|
||||
|
||||
class SiteTranslationImporter extends Importer
|
||||
{
|
||||
protected static ?string $model = SiteTranslation::class;
|
||||
|
||||
public static function getColumns(): array
|
||||
{
|
||||
$columns = [
|
||||
ImportColumn::make('key')
|
||||
->label('Key')
|
||||
->requiredMapping()
|
||||
->rules(['required']),
|
||||
];
|
||||
|
||||
try {
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
foreach ($languages as $language) {
|
||||
$columns[] = ImportColumn::make($language->code)
|
||||
->label($language->name . " ({$language->code})");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
//
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function resolveRecord(): ?SiteTranslation
|
||||
{
|
||||
if (empty($this->data['key'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Key üzerinden kaydı bul veya yeni oluştur
|
||||
// Not: key alanı text olduğu için tam eşleşme arıyoruz
|
||||
return SiteTranslation::firstOrNew([
|
||||
'key' => $this->data['key'],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function beforeSave(): void
|
||||
{
|
||||
$translations = $this->record->translations ?? [];
|
||||
|
||||
$languages = Language::where('is_active', true)->pluck('code')->toArray();
|
||||
|
||||
foreach ($languages as $code) {
|
||||
if (array_key_exists($code, $this->data)) {
|
||||
$value = $this->data[$code];
|
||||
|
||||
// Boş string de olsa güncelleyelim, null ise (CSV'de yoksa) dokunmayalım
|
||||
if ($value !== null) {
|
||||
$translations[$code] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->record->translations = $translations;
|
||||
|
||||
// Hash'i manuel oluşturmaya gerek yok, model event'i halledecek ama
|
||||
// new record ise ve event çalışmazsa diye garantiye alalım (import toplu insert yapabilir mi? Hayır record record işler)
|
||||
if (!$this->record->exists && empty($this->record->hash)) {
|
||||
$this->record->hash = md5($this->record->key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getCompletedNotificationBody(Import $import): string
|
||||
{
|
||||
$body = 'Your site translation import has completed and ' . number_format($import->successful_rows) . ' ' . str('row')->plural($import->successful_rows) . ' imported.';
|
||||
|
||||
if ($failedRowsCount = $import->getFailedRowsCount()) {
|
||||
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to import.';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
if (!function_exists('setting')) {
|
||||
/**
|
||||
@@ -12,7 +14,35 @@ if (!function_exists('setting')) {
|
||||
*/
|
||||
function setting(string $key, $default = null)
|
||||
{
|
||||
return Setting::get($key, $default);
|
||||
// Cache settings to avoid database queries on every call
|
||||
// Cache will store the full setting object to access type info
|
||||
$setting = Cache::remember("app_setting_{$key}", 60 * 60 * 24, function () use ($key) {
|
||||
return Setting::where('key', $key)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
});
|
||||
|
||||
if (!$setting) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$value = $setting->value;
|
||||
|
||||
// Handle file/image types
|
||||
if (in_array($setting->type, ['file', 'image']) || $key === 'home_hero') {
|
||||
if (empty($value)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// If value is already a URL, return it
|
||||
if (filter_var($value, FILTER_VALIDATE_URL)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Return storage URL
|
||||
return Storage::url($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\Translation;
|
||||
use App\Models\SiteTranslation;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
if (!function_exists('current_language')) {
|
||||
@@ -221,6 +222,93 @@ if (!function_exists('get_translation_status_color')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('flag_emoji_from_country_code')) {
|
||||
/**
|
||||
* Build a flag emoji from a two-letter country/region code (e.g. TR → 🇹🇷).
|
||||
*/
|
||||
function flag_emoji_from_country_code(string $countryCode): string
|
||||
{
|
||||
$countryCode = strtoupper(substr($countryCode, 0, 2));
|
||||
$flag = '';
|
||||
|
||||
for ($i = 0; $i < strlen($countryCode); $i++) {
|
||||
$flag .= mb_chr(127397 + ord($countryCode[$i]));
|
||||
}
|
||||
|
||||
return $flag;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('country_code_from_flag_emoji')) {
|
||||
/**
|
||||
* Extract a two-letter code from a flag emoji (e.g. 🇬🇧 → GB).
|
||||
*/
|
||||
function country_code_from_flag_emoji(?string $flag): ?string
|
||||
{
|
||||
if (empty($flag)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$code = '';
|
||||
|
||||
foreach (mb_str_split($flag) as $char) {
|
||||
$ord = mb_ord($char);
|
||||
|
||||
if ($ord >= 0x1F1E6 && $ord <= 0x1F1FF) {
|
||||
$code .= chr(ord('A') + ($ord - 0x1F1E6));
|
||||
}
|
||||
}
|
||||
|
||||
return strlen($code) === 2 ? $code : null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('resolve_language_flag')) {
|
||||
/**
|
||||
* Resolve flag emoji for a language (stored flag or generated from code).
|
||||
*/
|
||||
function resolve_language_flag(Language|string $language): string
|
||||
{
|
||||
if (is_string($language)) {
|
||||
$language = Language::findByCode($language);
|
||||
}
|
||||
|
||||
if (! $language) {
|
||||
return '🌐';
|
||||
}
|
||||
|
||||
if (! empty($language->flag) && $language->flag !== '🌐') {
|
||||
return $language->flag;
|
||||
}
|
||||
|
||||
return flag_emoji_from_country_code($language->code);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_language_short_code')) {
|
||||
/**
|
||||
* Short display code for language (from flag region, e.g. GB, TR, SE).
|
||||
*/
|
||||
function get_language_short_code(Language|string $language): string
|
||||
{
|
||||
if (is_string($language)) {
|
||||
$model = Language::findByCode($language);
|
||||
|
||||
return $model ? get_language_short_code($model) : strtoupper($language);
|
||||
}
|
||||
|
||||
$fromFlag = country_code_from_flag_emoji($language->flag);
|
||||
|
||||
if ($fromFlag) {
|
||||
return $fromFlag;
|
||||
}
|
||||
|
||||
$fromResolved = country_code_from_flag_emoji(resolve_language_flag($language));
|
||||
|
||||
return $fromResolved ?: strtoupper($language->code);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('get_language_flag')) {
|
||||
/**
|
||||
* Get language flag emoji or icon
|
||||
@@ -228,7 +316,8 @@ if (!function_exists('get_language_flag')) {
|
||||
function get_language_flag(string $languageCode): string
|
||||
{
|
||||
$language = Language::findByCode($languageCode);
|
||||
return $language ? ($language->flag ?? '🌐') : '🌐';
|
||||
|
||||
return $language ? resolve_language_flag($language) : '🌐';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,3 +368,42 @@ if (!function_exists('get_translation_value')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('t')) {
|
||||
/**
|
||||
* Get site translation for a given key.
|
||||
*
|
||||
* @param string $key
|
||||
* @return string
|
||||
*/
|
||||
function t(string $key): string
|
||||
{
|
||||
if (empty($key)) {
|
||||
return '';
|
||||
}
|
||||
return SiteTranslation::getTranslation($key);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('locale_upper')) {
|
||||
/**
|
||||
* Locale-aware uppercase (Turkish: i→İ, ı→I, etc.)
|
||||
*/
|
||||
function locale_upper(string $text, ?string $locale = null): string
|
||||
{
|
||||
$locale = $locale ?? app()->getLocale();
|
||||
|
||||
if (str_starts_with($locale, 'tr')) {
|
||||
return mb_strtoupper(
|
||||
str_replace(
|
||||
['i', 'ı', 'ğ', 'ü', 'ş', 'ö', 'ç'],
|
||||
['İ', 'I', 'Ğ', 'Ü', 'Ş', 'Ö', 'Ç'],
|
||||
$text
|
||||
),
|
||||
'UTF-8'
|
||||
);
|
||||
}
|
||||
|
||||
return mb_strtoupper($text, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SiteTranslation;
|
||||
use App\Models\Language;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class SiteTranslationController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$query = SiteTranslation::query();
|
||||
|
||||
// Search
|
||||
if ($request->has('search')) {
|
||||
$search = $request->get('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('key', 'like', "%{$search}%")
|
||||
->orWhereRaw("JSON_SEARCH(translations, 'one', ?, NULL, '$[*]') IS NOT NULL", ["%{$search}%"]);
|
||||
});
|
||||
}
|
||||
|
||||
// Sorting
|
||||
$sortField = $request->get('sort', 'id');
|
||||
$sortOrder = $request->get('order', 'desc');
|
||||
$query->orderBy($sortField, $sortOrder);
|
||||
|
||||
// Pagination
|
||||
$page = $request->get('page', 1);
|
||||
$pageSize = $request->get('pageSize', 20);
|
||||
|
||||
$totalCount = $query->count();
|
||||
$items = $query->skip(($page - 1) * $pageSize)->take($pageSize)->get();
|
||||
|
||||
// Format data for DataGrid
|
||||
$data = $items->map(function ($item) {
|
||||
$formatted = [
|
||||
'id' => $item->id,
|
||||
'key' => $item->key,
|
||||
'hash' => $item->hash,
|
||||
'created_at' => $item->created_at?->toDateTimeString(),
|
||||
'updated_at' => $item->updated_at?->toDateTimeString(),
|
||||
];
|
||||
|
||||
// Add translation fields for each active language
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
$translations = $item->translations ?? [];
|
||||
|
||||
foreach ($languages as $language) {
|
||||
$formatted[$language->code] = $translations[$language->code] ?? '';
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'data' => $data,
|
||||
'totalCount' => $totalCount,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'key' => 'required|string|max:500',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$hash = md5($request->key);
|
||||
|
||||
// Check if translation already exists
|
||||
$exists = SiteTranslation::where('hash', $hash)->first();
|
||||
if ($exists) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Bu key zaten mevcut.',
|
||||
], 409);
|
||||
}
|
||||
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
$translations = [];
|
||||
|
||||
foreach ($languages as $language) {
|
||||
if ($request->has($language->code)) {
|
||||
$translations[$language->code] = $request->get($language->code, '');
|
||||
}
|
||||
}
|
||||
|
||||
$translation = SiteTranslation::create([
|
||||
'hash' => $hash,
|
||||
'key' => $request->key,
|
||||
'translations' => $translations,
|
||||
]);
|
||||
|
||||
Cache::forget("site_translation_{$hash}");
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $translation->load('translations'),
|
||||
'message' => 'Çeviri başarıyla oluşturuldu.',
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, $id): JsonResponse
|
||||
{
|
||||
$translation = SiteTranslation::findOrFail($id);
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'key' => 'sometimes|required|string|max:500',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
$translations = $translation->translations ?? [];
|
||||
|
||||
// Update translations
|
||||
foreach ($languages as $language) {
|
||||
if ($request->has($language->code)) {
|
||||
$translations[$language->code] = $request->get($language->code, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Update key if provided
|
||||
if ($request->has('key') && $request->key !== $translation->key) {
|
||||
$translation->key = $request->key;
|
||||
$translation->hash = md5($request->key);
|
||||
}
|
||||
|
||||
$translation->translations = $translations;
|
||||
$translation->save();
|
||||
|
||||
Cache::forget("site_translation_{$translation->hash}");
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'data' => $translation->fresh(),
|
||||
'message' => 'Çeviri başarıyla güncellendi.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update multiple resources.
|
||||
*/
|
||||
public function batchUpdate(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'changes' => 'required|array',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$changes = $request->get('changes', []);
|
||||
$updated = [];
|
||||
$errors = [];
|
||||
|
||||
foreach ($changes as $change) {
|
||||
try {
|
||||
// ID can be in 'key' field (integer) or directly in 'key' field
|
||||
$id = is_numeric($change['key'] ?? null)
|
||||
? (int)$change['key']
|
||||
: ($change['key']['id'] ?? null);
|
||||
|
||||
if (!$id) {
|
||||
$errors[] = 'ID bulunamadı.';
|
||||
continue;
|
||||
}
|
||||
|
||||
$translation = SiteTranslation::find($id);
|
||||
if (!$translation) {
|
||||
$errors[] = "ID {$id} için kayıt bulunamadı.";
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $change['data'] ?? [];
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
$translations = $translation->translations ?? [];
|
||||
|
||||
// Update translations
|
||||
foreach ($languages as $language) {
|
||||
if (isset($data[$language->code])) {
|
||||
$translations[$language->code] = $data[$language->code];
|
||||
}
|
||||
}
|
||||
|
||||
// Update key if provided
|
||||
if (isset($data['key']) && $data['key'] !== $translation->key) {
|
||||
$translation->key = $data['key'];
|
||||
$translation->hash = md5($data['key']);
|
||||
}
|
||||
|
||||
$translation->translations = $translations;
|
||||
$translation->save();
|
||||
|
||||
Cache::forget("site_translation_{$translation->hash}");
|
||||
|
||||
$updated[] = $translation->id;
|
||||
} catch (\Exception $e) {
|
||||
$id = is_numeric($change['key'] ?? null)
|
||||
? (int)$change['key']
|
||||
: ($change['key']['id'] ?? 'unknown');
|
||||
$errors[] = "ID {$id} için hata: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => count($errors) === 0,
|
||||
'updated' => $updated,
|
||||
'errors' => $errors,
|
||||
'message' => count($updated) . ' kayıt güncellendi.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy($id): JsonResponse
|
||||
{
|
||||
$translation = SiteTranslation::findOrFail($id);
|
||||
$hash = $translation->hash;
|
||||
|
||||
$translation->delete();
|
||||
Cache::forget("site_translation_{$hash}");
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Çeviri başarıyla silindi.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch delete multiple resources.
|
||||
*/
|
||||
public function batchDestroy(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'ids' => 'required|array',
|
||||
'ids.*' => 'required|integer|exists:site_translations,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$ids = $request->get('ids', []);
|
||||
$translations = SiteTranslation::whereIn('id', $ids)->get();
|
||||
|
||||
foreach ($translations as $translation) {
|
||||
Cache::forget("site_translation_{$translation->hash}");
|
||||
}
|
||||
|
||||
SiteTranslation::whereIn('id', $ids)->delete();
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => count($ids) . ' kayıt başarıyla silindi.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ use App\Models\BlogComment;
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Models\Page;
|
||||
use App\Support\BlogStructuredData;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
@@ -40,47 +42,179 @@ class BlogController extends Controller
|
||||
$query = $query->whereJsonContains('tags', $request->tag);
|
||||
}
|
||||
|
||||
// Search filtresi
|
||||
if ($query && $request->has('search') && $request->search) {
|
||||
$search = $request->search;
|
||||
$query = $query->where(function($q) use ($search) {
|
||||
$q->where('title', 'like', '%' . $search . '%')
|
||||
->orWhere('content', 'like', '%' . $search . '%')
|
||||
->orWhere('excerpt', 'like', '%' . $search . '%');
|
||||
});
|
||||
}
|
||||
|
||||
$posts = $query
|
||||
? $query->latest('published_at')->paginate(12)
|
||||
: collect();
|
||||
|
||||
// Render Header Template - En son aktif olan header template'i kullan
|
||||
// Blog sayfa ayarlarını bul (varsa)
|
||||
$blogPage = Page::where('slug', 'blog')->first();
|
||||
|
||||
// Render Header Template
|
||||
$renderedHeader = null;
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
$headerTemplate = null;
|
||||
|
||||
// 1. Öncelik: Blog sayfasında seçili header template
|
||||
if ($blogPage && $blogPage->header_template_id) {
|
||||
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
|
||||
}
|
||||
|
||||
// 2. Öncelik: Varsayılan aktif template (KALDIRILDI - Default static header kullanılmalı)
|
||||
/*
|
||||
if (!$headerTemplate) {
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
*/
|
||||
|
||||
if ($headerTemplate) {
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
|
||||
$mergedData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$templateDefaults
|
||||
$mergedData,
|
||||
$blogPage
|
||||
);
|
||||
}
|
||||
|
||||
// Render Footer Template - En son aktif olan footer template'i kullan
|
||||
// Render Footer Template
|
||||
$renderedFooter = null;
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
$footerTemplate = null;
|
||||
|
||||
// 1. Öncelik: Blog sayfasında seçili footer template
|
||||
if ($blogPage && $blogPage->footer_template_id) {
|
||||
$footerTemplate = FooterTemplate::find($blogPage->footer_template_id);
|
||||
}
|
||||
|
||||
// 2. Öncelik: Varsayılan aktif template
|
||||
if (!$footerTemplate) {
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($footerTemplate) {
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$pageData = $blogPage ? ($blogPage->footer_data ?? []) : [];
|
||||
$mergedData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$templateDefaults
|
||||
$mergedData,
|
||||
$blogPage
|
||||
);
|
||||
}
|
||||
|
||||
// Kategorileri al (filtreleme için)
|
||||
$categories = class_exists(\App\Models\BlogCategory::class) ? \App\Models\BlogCategory::where('is_active', true)->orderBy('sort_order')->get() : collect();
|
||||
|
||||
// Elemis Widget'ları için veri hazırlığı
|
||||
$carouselPosts = collect();
|
||||
$highlightCategories = collect();
|
||||
$popularPosts = collect();
|
||||
$sidebarCategories = collect();
|
||||
$tags = collect();
|
||||
|
||||
if (!$request->ajax()) {
|
||||
if (class_exists(Blog::class)) {
|
||||
// Karusel: Öne çıkarılan ya da en güncel 5 yazı
|
||||
$carouselPosts = Blog::with(['category', 'author'])
|
||||
->published()
|
||||
->featured()
|
||||
->latest('published_at')
|
||||
->take(5)
|
||||
->get();
|
||||
if ($carouselPosts->isEmpty()) {
|
||||
$carouselPosts = Blog::with(['category', 'author'])
|
||||
->published()
|
||||
->latest('published_at')
|
||||
->take(5)
|
||||
->get();
|
||||
}
|
||||
|
||||
// Popüler Yazılar: En çok okunan 3 yazı
|
||||
$popularPosts = Blog::with(['category', 'author'])
|
||||
->published()
|
||||
->orderBy('view_count', 'desc')
|
||||
->take(3)
|
||||
->get();
|
||||
|
||||
// Etiket Bulutu: Tüm yayınlanan yazılardaki benzersiz etiketler
|
||||
$allTags = Blog::published()
|
||||
->whereNotNull('tags')
|
||||
->pluck('tags');
|
||||
$tagsCollected = collect();
|
||||
foreach ($allTags as $postTags) {
|
||||
if (is_array($postTags)) {
|
||||
foreach ($postTags as $tag) {
|
||||
$tagsCollected->push($tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
$tags = $tagsCollected->unique()->take(15);
|
||||
}
|
||||
|
||||
if (class_exists(\App\Models\BlogCategory::class)) {
|
||||
// Hoş Geldiniz bölümü altındaki 4'lü kategori vurgusu
|
||||
$highlightCategories = \App\Models\BlogCategory::where('is_active', true)
|
||||
->withCount('blogs')
|
||||
->has('blogs')
|
||||
->orderBy('sort_order')
|
||||
->take(4)
|
||||
->get();
|
||||
if ($highlightCategories->count() < 4) {
|
||||
$highlightCategories = \App\Models\BlogCategory::where('is_active', true)
|
||||
->orderBy('sort_order')
|
||||
->take(4)
|
||||
->get();
|
||||
}
|
||||
|
||||
// Sidebar Kategorileri: Yazı sayısıyla birlikte
|
||||
$sidebarCategories = \App\Models\BlogCategory::where('is_active', true)
|
||||
->withCount('blogs')
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
return view('blog.partials.posts', [
|
||||
'posts' => $posts
|
||||
]);
|
||||
}
|
||||
|
||||
$pageUrl = url()->current();
|
||||
|
||||
return view('blog.index', [
|
||||
'settings' => $settings,
|
||||
'posts' => $posts,
|
||||
'categories' => $categories,
|
||||
'carouselPosts' => $carouselPosts,
|
||||
'highlightCategories' => $highlightCategories,
|
||||
'popularPosts' => $popularPosts,
|
||||
'sidebarCategories' => $sidebarCategories,
|
||||
'tags' => $tags,
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => [
|
||||
'title' => __('blog.meta-index-title'),
|
||||
'description' => __('blog.meta-index-description'),
|
||||
],
|
||||
'structuredData' => $posts instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
? BlogStructuredData::forIndex($posts, $pageUrl)
|
||||
: null,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -132,17 +266,26 @@ class BlogController extends Controller
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
// Render Header Template - En son aktif olan header template'i kullan
|
||||
// Blog sayfa ayarlarını bul (varsa)
|
||||
$blogPage = Page::where('slug', 'blog')->first();
|
||||
|
||||
// Render Header Template
|
||||
$renderedHeader = null;
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
$headerTemplate = null;
|
||||
|
||||
// 1. Öncelik: Blog sayfasında seçili header template
|
||||
if ($blogPage && $blogPage->header_template_id) {
|
||||
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
|
||||
}
|
||||
|
||||
if ($headerTemplate) {
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
|
||||
$mergedData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$templateDefaults,
|
||||
$mergedData,
|
||||
$post
|
||||
);
|
||||
}
|
||||
@@ -162,6 +305,8 @@ class BlogController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$pageUrl = route('blog.show', $post->slug);
|
||||
|
||||
return view('blog.show', [
|
||||
'settings' => $settings,
|
||||
'post' => $post,
|
||||
@@ -173,7 +318,9 @@ class BlogController extends Controller
|
||||
'title' => method_exists($post, 'translate') ? ($post->translate('meta_title') ?: $post->translate('title')) : ($post->meta_title ?? $post->title),
|
||||
'description' => method_exists($post, 'translate') ? ($post->translate('meta_description') ?: $post->translate('excerpt')) : ($post->meta_description ?? $post->excerpt),
|
||||
'image' => $post->featured_image ? asset('storage/' . $post->featured_image) : null,
|
||||
'og_type' => 'article',
|
||||
],
|
||||
'structuredData' => BlogStructuredData::forShow($post, $pageUrl),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\CareerApplication;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CareerController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return view('front.career.index', [
|
||||
'meta' => [
|
||||
'title' => __('career.job_application_title', ['default' => 'İş Başvurusu']),
|
||||
'description' => __('career.job_application_description', ['default' => 'Ekibimize katılmak için aşağıdaki formu doldurarak CV\'nizi iletebilirsiniz.']),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function internship()
|
||||
{
|
||||
return view('front.career.internship', [
|
||||
'meta' => [
|
||||
'title' => __('career.internship_title', ['default' => 'Staj Başvurusu']),
|
||||
'description' => __('career.internship_description', ['default' => 'Uzaktan staj programımız hakkında bilgi alın ve başvurunuzu yapın.']),
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'phone' => 'nullable|string|max:20',
|
||||
'cv' => 'required|file|mimes:pdf,doc,docx|max:5120', // Max 5MB
|
||||
'message' => 'nullable|string',
|
||||
];
|
||||
|
||||
if ($request->type === 'job') {
|
||||
$rules['nda'] = 'required|file|mimes:pdf|max:5120';
|
||||
$rules['contract'] = 'required|file|mimes:pdf|max:5120';
|
||||
$rules['id_photocopy'] = 'required|file|mimes:pdf,jpg,jpeg,png|max:5120'; // Max 5MB
|
||||
$rules['git_knowledge'] = 'accepted';
|
||||
$rules['ai_usage'] = 'accepted';
|
||||
}
|
||||
|
||||
$request->validate($rules);
|
||||
|
||||
$cvPath = $request->file('cv') ? $request->file('cv')->store('cvs', 'public') : null;
|
||||
$ndaPath = $request->file('nda') ? $request->file('nda')->store('ndas', 'public') : null;
|
||||
$contractPath = $request->file('contract') ? $request->file('contract')->store('contracts', 'public') : null;
|
||||
$idPhotocopyPath = $request->file('id_photocopy') ? $request->file('id_photocopy')->store('id_photocopies', 'public') : null;
|
||||
|
||||
CareerApplication::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
'type' => $request->type ?? 'job',
|
||||
'cv_path' => $cvPath,
|
||||
'nda_path' => $ndaPath,
|
||||
'contract_path' => $contractPath,
|
||||
'id_photocopy_path' => $idPhotocopyPath,
|
||||
'git_knowledge' => $request->has('git_knowledge'),
|
||||
'ai_usage' => $request->has('ai_usage'),
|
||||
'message' => $request->message,
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
if ($request->ajax()) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => __('career.success_message')
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->back()->with('success', __('career.success_message'));
|
||||
}
|
||||
|
||||
public function internLoginForm()
|
||||
{
|
||||
if (session()->has('intern_id')) {
|
||||
return redirect()->route('intern.dashboard');
|
||||
}
|
||||
return view('front.career.intern_login', [
|
||||
'meta' => [
|
||||
'title' => 'Stajyer Girişi',
|
||||
'description' => 'İmzalı staj belgelerinize erişmek için lütfen giriş yapın.',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function internLogin(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
$intern = CareerApplication::where('username', $request->username)
|
||||
->where('type', 'internship')
|
||||
->first();
|
||||
|
||||
if (!$intern || !\Illuminate\Support\Facades\Hash::check($request->password, $intern->password)) {
|
||||
return redirect()->back()
|
||||
->withInput($request->only('username'))
|
||||
->withErrors([
|
||||
'username' => 'Girdiğiniz kullanıcı adı veya şifre hatalı.',
|
||||
]);
|
||||
}
|
||||
|
||||
session(['intern_id' => $intern->id]);
|
||||
|
||||
return redirect()->route('intern.dashboard')->with('success', 'Başarıyla giriş yapıldı.');
|
||||
}
|
||||
|
||||
public function internDashboard()
|
||||
{
|
||||
if (!session()->has('intern_id')) {
|
||||
return redirect()->route('intern.login')->with('error', 'Lütfen giriş yapın.');
|
||||
}
|
||||
|
||||
$intern = CareerApplication::findOrFail(session('intern_id'));
|
||||
return view('front.career.intern_dashboard', [
|
||||
'intern' => $intern,
|
||||
'meta' => [
|
||||
'title' => 'Stajyer Paneli',
|
||||
'description' => 'Belgelerinizi buradan indirebilirsiniz.',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function uploadInternshipForm(Request $request)
|
||||
{
|
||||
if (!session()->has('intern_id')) {
|
||||
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'internship_form' => 'required|file|mimes:pdf,docx,jpg,png,jpeg|max:51200',
|
||||
'internship_start_date' => 'required|date',
|
||||
'internship_total_days' => 'required|integer|min:1',
|
||||
], [
|
||||
'internship_form.required' => 'Lütfen bir dosya seçin.',
|
||||
'internship_form.file' => 'Yüklenen öğe geçerli bir dosya olmalıdır.',
|
||||
'internship_form.mimes' => 'Yalnızca PDF, DOCX, JPG ve PNG formatındaki dosyalar kabul edilir.',
|
||||
'internship_form.max' => 'Dosya boyutu en fazla 50MB olabilir.',
|
||||
'internship_start_date.required' => 'Lütfen staj başlangıç tarihini girin.',
|
||||
'internship_start_date.date' => 'Geçerli bir başlangıç tarihi girin.',
|
||||
'internship_total_days.required' => 'Lütfen staj süresini girin.',
|
||||
'internship_total_days.integer' => 'Staj süresi geçerli bir tam sayı olmalıdır.',
|
||||
'internship_total_days.min' => 'Staj süresi en az 1 gün olmalıdır.',
|
||||
]);
|
||||
|
||||
$intern = CareerApplication::findOrFail(session('intern_id'));
|
||||
|
||||
if ($request->hasFile('internship_form')) {
|
||||
// Delete old file if exists
|
||||
if ($intern->to_be_signed_internship_form_path) {
|
||||
Storage::disk('public')->delete($intern->to_be_signed_internship_form_path);
|
||||
}
|
||||
|
||||
$path = $request->file('internship_form')->store('to_be_signed_interns', 'public');
|
||||
|
||||
// Calculate end date based on weekdays and duration
|
||||
$startDate = \Carbon\Carbon::parse($request->internship_start_date);
|
||||
$daysToAdd = intval($request->internship_total_days);
|
||||
|
||||
$endDate = $startDate->copy();
|
||||
$count = 0;
|
||||
$temp = $startDate->copy();
|
||||
|
||||
while ($count < $daysToAdd) {
|
||||
if ($temp->isWeekend()) {
|
||||
$temp->addDay();
|
||||
continue;
|
||||
}
|
||||
$endDate = $temp->copy();
|
||||
$temp->addDay();
|
||||
$count++;
|
||||
}
|
||||
|
||||
$intern->update([
|
||||
'to_be_signed_internship_form_path' => $path,
|
||||
'internship_start_date' => $request->internship_start_date,
|
||||
'internship_end_date' => $endDate->format('Y-m-d'),
|
||||
'internship_total_days' => $daysToAdd,
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'İmzalanacak staj formunuz ve staj tarihleri başarıyla kaydedildi.');
|
||||
}
|
||||
|
||||
return redirect()->back()->with('error', 'Dosya yüklenirken bir hata oluştu.');
|
||||
}
|
||||
|
||||
public function saveGithubRepo(Request $request)
|
||||
{
|
||||
if (!session()->has('intern_id')) {
|
||||
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'github_repo' => 'required|string|url|max:255',
|
||||
], [
|
||||
'github_repo.required' => 'Lütfen Github depo URL\'sini girin.',
|
||||
'github_repo.url' => 'Geçerli bir URL girilmelidir.',
|
||||
'github_repo.max' => 'Github depo URL\'si en fazla 255 karakter olabilir.',
|
||||
]);
|
||||
|
||||
$intern = CareerApplication::findOrFail(session('intern_id'));
|
||||
|
||||
// Sanitize & format github URL
|
||||
$repoUrl = trim($request->github_repo);
|
||||
|
||||
$intern->update([
|
||||
'github_repo' => $repoUrl
|
||||
]);
|
||||
|
||||
return redirect()->back()->with('success', 'Github deposu başarıyla güncellendi.');
|
||||
}
|
||||
|
||||
public function downloadMarkdown()
|
||||
{
|
||||
if (!session()->has('intern_id')) {
|
||||
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
|
||||
}
|
||||
$intern = CareerApplication::findOrFail(session('intern_id'));
|
||||
$repo = $intern->github_repo;
|
||||
if (!$repo) {
|
||||
return redirect()->back()->with('error', 'Lütfen önce Github deposunu tanımlayın.');
|
||||
}
|
||||
|
||||
// Parse owner and repo
|
||||
// E.g., https://github.com/owner/repo or owner/repo
|
||||
$repoClean = str_replace(['https://github.com/', 'http://github.com/', 'https://www.github.com/', 'http://www.github.com/'], '', $repo);
|
||||
$repoClean = trim($repoClean, '/');
|
||||
$parts = explode('/', $repoClean);
|
||||
if (count($parts) < 2) {
|
||||
return redirect()->back()->with('error', 'Geçersiz Github depo formatı.');
|
||||
}
|
||||
$owner = $parts[0];
|
||||
$repoName = explode('.', $parts[1])[0]; // Remove .git if exists
|
||||
|
||||
// Fetch commits
|
||||
$url = "https://api.github.com/repos/{$owner}/{$repoName}/commits?per_page=100";
|
||||
|
||||
$opts = [
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => [
|
||||
'User-Agent: PHP-Github-Client',
|
||||
]
|
||||
]
|
||||
];
|
||||
$context = stream_context_create($opts);
|
||||
$response = @file_get_contents($url, false, $context);
|
||||
|
||||
if ($response === false) {
|
||||
return redirect()->back()->with('error', 'Github API\'den commit bilgileri alınamadı. Deponuzun public olduğundan emin olun.');
|
||||
}
|
||||
|
||||
$commits = json_decode($response, true);
|
||||
if (!is_array($commits)) {
|
||||
return redirect()->back()->with('error', 'Github API yanıtı çözümlenemedi.');
|
||||
}
|
||||
|
||||
// Sort commits chronological (ascending)
|
||||
$commits = array_reverse($commits);
|
||||
|
||||
// Group commits by date (YYYY-MM-DD)
|
||||
$grouped = [];
|
||||
foreach ($commits as $c) {
|
||||
$dateStr = $c['commit']['author']['date'] ?? '';
|
||||
if ($dateStr) {
|
||||
$dateOnly = \Carbon\Carbon::parse($dateStr)->format('Y-m-d');
|
||||
$grouped[$dateOnly][] = $c;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort dates chronological (ascending)
|
||||
ksort($grouped);
|
||||
|
||||
// Generate Markdown content
|
||||
$md = "# Staj Günlüğü - " . $intern->name . "\n";
|
||||
$md .= "Depo: https://github.com/{$owner}/{$repoName}\n\n";
|
||||
|
||||
$day = 1;
|
||||
foreach ($grouped as $date => $dayCommits) {
|
||||
$formattedDate = \Carbon\Carbon::parse($date)->format('d.m.Y');
|
||||
$md .= "## {$day}. Gün ({$formattedDate})\n";
|
||||
foreach ($dayCommits as $c) {
|
||||
$message = trim($c['commit']['message'] ?? '');
|
||||
$time = \Carbon\Carbon::parse($c['commit']['author']['date'] ?? '')->format('H:i');
|
||||
$hash = substr($c['sha'] ?? '', 0, 7);
|
||||
$md .= "- [{$time}] `{$hash}`: {$message}\n";
|
||||
}
|
||||
$md .= "\n";
|
||||
$day++;
|
||||
}
|
||||
|
||||
$fileName = str()->slug($intern->name) . "-staj-gunlugu.md";
|
||||
|
||||
return response($md, 200, [
|
||||
'Content-Type' => 'text/markdown; charset=UTF-8',
|
||||
'Content-Disposition' => 'attachment; filename="' . $fileName . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
public function internLogout()
|
||||
{
|
||||
session()->forget('intern_id');
|
||||
return redirect()->route('intern.login')->with('success', 'Başarıyla çıkış yapıldı.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\MusicProduction;
|
||||
use App\Models\Setting;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Support\MusicProductionStructuredData;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class MusicProductionController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
// Get Settings
|
||||
$settings = new \stdClass();
|
||||
$allSettings = Setting::query()->where('is_active', true)->get();
|
||||
foreach ($allSettings as $setting) {
|
||||
$settings->{$setting->key} = $setting->value;
|
||||
}
|
||||
|
||||
$search = trim((string) request('q', ''));
|
||||
|
||||
$productionsQuery = MusicProduction::active()->ordered();
|
||||
|
||||
if ($search !== '') {
|
||||
$locale = app()->getLocale();
|
||||
|
||||
$productionsQuery->where(function ($query) use ($search, $locale) {
|
||||
$query->where('title', 'like', "%{$search}%")
|
||||
->orWhere('client_name', 'like', "%{$search}%")
|
||||
->orWhere('slug', 'like', "%{$search}%")
|
||||
->orWhereHas('translations', function ($translationQuery) use ($search, $locale) {
|
||||
$translationQuery
|
||||
->where('status', 'published')
|
||||
->whereIn('field_name', ['title', 'client_name', 'content'])
|
||||
->where('field_value', 'like', "%{$search}%")
|
||||
->where('language_code', $locale);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$productions = $productionsQuery->paginate(9)->withQueryString();
|
||||
|
||||
// --- Footer Logic ---
|
||||
$renderedFooter = null;
|
||||
$defaultFooterId = $settings->default_footer ?? null;
|
||||
|
||||
if ($defaultFooterId) {
|
||||
$footerTemplate = FooterTemplate::query()->find($defaultFooterId);
|
||||
if ($footerTemplate) {
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$mergedFooterData = array_merge($templateDefaults, []);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$mergedFooterData,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$canonicalUrl = route('music-productions.index');
|
||||
$pageUrl = $search !== '' ? $canonicalUrl : url()->current();
|
||||
|
||||
$meta = [
|
||||
'title' => __('music_productions.meta-index-title'),
|
||||
'description' => __('music_productions.meta-index-description'),
|
||||
'image' => asset('assets/music-production.png'),
|
||||
'image_alt' => __('music_productions.meta-index-title'),
|
||||
'canonical' => $canonicalUrl,
|
||||
'robots' => $search !== '' ? 'noindex, follow' : 'index, follow',
|
||||
'og_type' => 'website',
|
||||
'locale' => app()->getLocale(),
|
||||
];
|
||||
|
||||
$structuredData = MusicProductionStructuredData::forIndex($productions, $pageUrl);
|
||||
|
||||
return view('front.music-productions.index', [
|
||||
'productions' => $productions,
|
||||
'search' => $search,
|
||||
'settings' => $settings,
|
||||
'header' => 'partials.header-center-nav',
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => $meta,
|
||||
'structuredData' => $structuredData,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($slug)
|
||||
{
|
||||
$production = MusicProduction::active()
|
||||
->where('slug', $slug)
|
||||
->firstOrFail();
|
||||
|
||||
// Get Settings
|
||||
$settings = new \stdClass();
|
||||
$allSettings = Setting::query()->where('is_active', true)->get();
|
||||
foreach ($allSettings as $setting) {
|
||||
$settings->{$setting->key} = $setting->value;
|
||||
}
|
||||
|
||||
// Fetch adjacent posts (Prev and Next)
|
||||
$prev = MusicProduction::active()
|
||||
->where(function($q) use ($production) {
|
||||
$q->where('sort_order', '<', $production->sort_order)
|
||||
->orWhere(function($sub) use ($production) {
|
||||
$sub->where('sort_order', $production->sort_order)
|
||||
->where('id', '<', $production->id);
|
||||
});
|
||||
})
|
||||
->orderBy('sort_order', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->first();
|
||||
|
||||
$next = MusicProduction::active()
|
||||
->where(function($q) use ($production) {
|
||||
$q->where('sort_order', '>', $production->sort_order)
|
||||
->orWhere(function($sub) use ($production) {
|
||||
$sub->where('sort_order', $production->sort_order)
|
||||
->where('id', '>', $production->id);
|
||||
});
|
||||
})
|
||||
->orderBy('sort_order', 'asc')
|
||||
->orderBy('id', 'asc')
|
||||
->first();
|
||||
|
||||
// --- Footer Logic ---
|
||||
$renderedFooter = null;
|
||||
$defaultFooterId = $settings->default_footer ?? null;
|
||||
|
||||
if ($defaultFooterId) {
|
||||
$footerTemplate = FooterTemplate::query()->find($defaultFooterId);
|
||||
if ($footerTemplate) {
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$mergedFooterData = array_merge($templateDefaults, []);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$mergedFooterData,
|
||||
$production
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$pageUrl = route('music-productions.show', $production->slug);
|
||||
$title = $production->translate('title');
|
||||
$description = Str::limit(strip_tags((string) $production->translate('content')), 160)
|
||||
?: __('music_productions.meta-index-description');
|
||||
|
||||
$meta = [
|
||||
'title' => $title,
|
||||
'description' => $description,
|
||||
'image' => $production->cover_image_url ?: asset('assets/music-production.png'),
|
||||
'image_alt' => $title,
|
||||
'canonical' => $pageUrl,
|
||||
'robots' => 'index, follow',
|
||||
'og_type' => 'article',
|
||||
'locale' => app()->getLocale(),
|
||||
];
|
||||
|
||||
$structuredData = MusicProductionStructuredData::forShow($production, $pageUrl);
|
||||
|
||||
return view('front.music-productions.show', [
|
||||
'production' => $production,
|
||||
'prev' => $prev,
|
||||
'next' => $next,
|
||||
'settings' => $settings,
|
||||
'header' => 'partials.header-center-nav',
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => $meta,
|
||||
'structuredData' => $structuredData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@ use App\Models\Setting;
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Support\PageStructuredData;
|
||||
use App\Support\PageTemplateHero;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
@@ -16,217 +19,242 @@ class PageController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 1) is_homepage işaretli ve published (en son güncellenen)
|
||||
$settings = $this->getSettings();
|
||||
|
||||
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||
->where('is_homepage', true)
|
||||
->where('status', 'published')
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
|
||||
|
||||
|
||||
// 1. Statik "home" Template Kontrolü
|
||||
if (view()->exists('templates.home')) {
|
||||
return $this->renderView('templates.home', $page, $settings);
|
||||
}
|
||||
|
||||
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
|
||||
|
||||
// Hiç sayfa yoksa: direkt home template'inin statik fallback'i ile render et
|
||||
// Hiç sayfa yoksa fallback
|
||||
if (!$page) {
|
||||
$meta = $this->getMeta(null, $settings);
|
||||
|
||||
return view('templates.home', [
|
||||
'page' => null,
|
||||
'settings' => $settings,
|
||||
'meta' => [
|
||||
'title' => $settings->default_meta_title ?? config('app.name'),
|
||||
'description' => $settings->default_meta_description ?? null,
|
||||
'image' => $settings->default_meta_image ?? null,
|
||||
],
|
||||
'meta' => $meta,
|
||||
'structuredData' => PageStructuredData::forPage(null, url('/'), $meta['title'] ?? null, $meta['description'] ?? null),
|
||||
]);
|
||||
}
|
||||
|
||||
$metaTitle = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||
: ($page->meta_title ?? $page->title ?? null);
|
||||
|
||||
$metaDescription = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
// Section verisini al - Hem eski hem yeni sistemi destekle
|
||||
$sections = $page->sections ?? $page->data ?? [];
|
||||
|
||||
// Yeni dinamik template sistemi için sections_data varsa onu da al
|
||||
$templatedSections = $page->templated_sections ?? collect([]);
|
||||
|
||||
// Template belirleme mantığı:
|
||||
// 1. Kullanıcı template seçmişse, onu kullan
|
||||
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
|
||||
// 3. Hiçbiri yoksa, 'generic' kullan
|
||||
$template = $page->template
|
||||
?? (($page->slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
|
||||
|
||||
$view = view()->exists("templates.$template")
|
||||
? "templates.$template"
|
||||
: 'templates.generic';
|
||||
|
||||
// Render Header Template
|
||||
$renderedHeader = null;
|
||||
$headerTemplate = $page->headerTemplate;
|
||||
|
||||
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
|
||||
if (!$headerTemplate) {
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($headerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $page->header_data ?? [];
|
||||
$mergedHeaderData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$mergedHeaderData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
// Render Footer Template
|
||||
$renderedFooter = null;
|
||||
$footerTemplate = $page->footerTemplate;
|
||||
|
||||
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
|
||||
if (!$footerTemplate) {
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($footerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$pageData = $page->footer_data ?? [];
|
||||
$mergedFooterData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$mergedFooterData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'settings' => $settings,
|
||||
'sections' => $sections,
|
||||
'templatedSections' => $templatedSections,
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => [
|
||||
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
|
||||
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
|
||||
'image' => $settings->default_meta_image ?? null,
|
||||
],
|
||||
]);
|
||||
return $this->renderPage($page, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a specific page by slug
|
||||
*/
|
||||
public function show($slug)
|
||||
public function show($slug)
|
||||
{
|
||||
return $this->handleShow($slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle nested page routes (e.g. /kurumsal/hakkimizda)
|
||||
*/
|
||||
public function showNested($parentSlug, $slug)
|
||||
{
|
||||
return $this->handleShow($slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle akademi.truncgil.com.tr subdomain
|
||||
*/
|
||||
public function showAkademi()
|
||||
{
|
||||
return $this->handleShow('truncgil-akademi');
|
||||
}
|
||||
|
||||
protected function handleShow($slug)
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
|
||||
if ($slug === 'akademi') {
|
||||
$slug = 'truncgil-akademi';
|
||||
}
|
||||
|
||||
// Redirection logic disabled for rendering the page locally on main domain.
|
||||
|
||||
// 1. Statik View Kontrolü
|
||||
if (view()->exists("templates.$slug")) {
|
||||
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||
->where('slug', $slug)
|
||||
->where('status', 'published')
|
||||
->first();
|
||||
|
||||
return $this->renderView("templates.$slug", $page, $settings);
|
||||
}
|
||||
|
||||
// 2. Normal Dinamik Akış (DB Kaydı Zorunlu)
|
||||
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||
->where('slug', $slug)
|
||||
->where('status', 'published')
|
||||
->firstOrFail();
|
||||
|
||||
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
|
||||
return $this->renderPage($page, $settings);
|
||||
}
|
||||
|
||||
$metaTitle = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||
: ($page->meta_title ?? $page->title ?? null);
|
||||
|
||||
$metaDescription = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
// Use parsed_sections for easier template usage (key-value format)
|
||||
$sections = $page->parsed_sections ?? $page->sections ?? $page->data ?? [];
|
||||
|
||||
// Yeni dinamik template sistemi için sections_data varsa onu da al
|
||||
$templatedSections = $page->templated_sections ?? collect([]);
|
||||
|
||||
// Template belirleme mantığı:
|
||||
// 1. Kullanıcı template seçmişse, onu kullan
|
||||
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
|
||||
// 3. Hiçbiri yoksa, 'generic' kullan
|
||||
/**
|
||||
* Render a page with full dynamic logic
|
||||
*/
|
||||
protected function renderPage(Page $page, $settings)
|
||||
{
|
||||
$template = $page->template
|
||||
?? (($slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
|
||||
?? (($page->is_homepage ?? false) ? 'home' : 'generic');
|
||||
|
||||
$view = view()->exists("templates.$template")
|
||||
? "templates.$template"
|
||||
: 'templates.generic';
|
||||
|
||||
return $this->renderView($view, $page, $settings);
|
||||
}
|
||||
|
||||
// Render Header Template
|
||||
/**
|
||||
* Shared rendering logic for both static and dynamic templates
|
||||
*/
|
||||
protected function renderView(string $view, ?Page $page, $settings)
|
||||
{
|
||||
$renderedHeader = null;
|
||||
$renderedFooter = null;
|
||||
|
||||
if ($page) {
|
||||
$renderedHeader = $this->renderHeader($page, $settings);
|
||||
$renderedFooter = $this->renderFooter($page, $settings);
|
||||
}
|
||||
|
||||
$meta = $this->getMeta($page, $settings);
|
||||
$pageUrl = url()->current();
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'pageHero' => $page ? PageTemplateHero::resolveForPage($page) : null,
|
||||
'settings' => $settings,
|
||||
'sections' => $page ? ($page->parsed_sections ?? $page->sections ?? $page->data ?? []) : [],
|
||||
'templatedSections' => $page ? ($page->templated_sections ?? collect([])) : collect([]),
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => $meta,
|
||||
'structuredData' => PageStructuredData::forPage(
|
||||
$page,
|
||||
$pageUrl,
|
||||
$meta['title'] ?? null,
|
||||
$meta['description'] ?? null,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings as an object
|
||||
*/
|
||||
protected function getSettings()
|
||||
{
|
||||
if (!class_exists(Setting::class)) {
|
||||
return (object) [];
|
||||
}
|
||||
|
||||
return (object) Setting::where('is_active', true)
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta information for a page
|
||||
*/
|
||||
protected function getMeta(?Page $page, $settings)
|
||||
{
|
||||
$metaTitle = null;
|
||||
$metaDescription = null;
|
||||
$metaImage = $settings->default_meta_image ?? null;
|
||||
|
||||
if ($page) {
|
||||
$metaTitle = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||
: ($page->meta_title ?? $page->title ?? null);
|
||||
|
||||
$metaDescription = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
if ($page->featured_image_url) {
|
||||
$metaImage = $page->featured_image_url;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
|
||||
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
|
||||
'image' => $metaImage,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render header template
|
||||
*/
|
||||
protected function renderHeader(Page $page, $settings)
|
||||
{
|
||||
// 1. Önce özel blade dosyası var mı kontrol et
|
||||
if (!empty($page->custom_header_blade) && view()->exists($page->custom_header_blade)) {
|
||||
return view($page->custom_header_blade, ['page' => $page])->render();
|
||||
}
|
||||
|
||||
// 2. Yoksa Header Template sistemini kullan
|
||||
$headerTemplate = $page->headerTemplate;
|
||||
|
||||
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
|
||||
if (!$headerTemplate) {
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
if (!$headerTemplate && isset($settings->default_header)) {
|
||||
$headerTemplate = HeaderTemplate::find($settings->default_header);
|
||||
}
|
||||
|
||||
if ($headerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $page->header_data ?? [];
|
||||
$mergedHeaderData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
return TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$mergedHeaderData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
// Render Footer Template
|
||||
$renderedFooter = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render footer template
|
||||
*/
|
||||
protected function renderFooter(Page $page, $settings)
|
||||
{
|
||||
// 1. Önce özel blade dosyası var mı kontrol et
|
||||
if (!empty($page->custom_footer_blade) && view()->exists($page->custom_footer_blade)) {
|
||||
return view($page->custom_footer_blade, ['page' => $page])->render();
|
||||
}
|
||||
|
||||
// 2. Yoksa Footer Template sistemini kullan
|
||||
$footerTemplate = $page->footerTemplate;
|
||||
|
||||
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
|
||||
if (!$footerTemplate) {
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
if (!$footerTemplate && isset($settings->default_footer)) {
|
||||
$footerTemplate = FooterTemplate::find($settings->default_footer);
|
||||
}
|
||||
|
||||
if ($footerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$pageData = $page->footer_data ?? [];
|
||||
$mergedFooterData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
return TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$mergedFooterData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'settings' => $settings,
|
||||
'sections' => $sections,
|
||||
'templatedSections' => $templatedSections,
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => [
|
||||
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
|
||||
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
|
||||
'image' => $settings->default_meta_image ?? null,
|
||||
],
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,6 +276,3 @@ public function show($slug)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function process(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'surname' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'phone' => 'required|string|max:20',
|
||||
'price' => 'required|numeric|min:1',
|
||||
'address' => 'nullable|string',
|
||||
'agreement' => 'accepted',
|
||||
], [
|
||||
'name.required' => 'Lütfen adınızı giriniz.',
|
||||
'surname.required' => 'Lütfen soyadınızı giriniz.',
|
||||
'email.required' => 'Lütfen e-posta adresinizi giriniz.',
|
||||
'email.email' => 'Geçerli bir e-posta adresi giriniz.',
|
||||
'phone.required' => 'Lütfen telefon numaranızı giriniz.',
|
||||
'price.required' => 'Lütfen ödeme tutarını giriniz.',
|
||||
'price.min' => 'Ödeme tutarı en az 1 TL olmalıdır.',
|
||||
'address.required' => 'Lütfen ödeme açıklamasını giriniz.',
|
||||
'agreement.accepted' => 'Lütfen sözleşmeyi onaylayınız.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
Log::info('Payment Validation Failed', $validator->errors()->toArray());
|
||||
|
||||
return back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$merchant_id = '127117';
|
||||
$merchant_key = 's9GH5FMuxFtqjxyJ';
|
||||
$merchant_salt = 'qTHTx3j4FjieXPP3';
|
||||
|
||||
$price = $request->input('price') * 100; // Krş cinsinden
|
||||
|
||||
// Get user IP (handling Cloudflare or standard)
|
||||
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
$user_ip = $ip;
|
||||
|
||||
$merchant_oid = time(); // Benzersiz sipariş no
|
||||
$email = $request->input('email');
|
||||
$payment_amount = $price;
|
||||
$currency = 'TL'; // Default
|
||||
$test_mode = 0; // Canlı mod
|
||||
|
||||
$user_name = $request->input('name').' '.$request->input('surname');
|
||||
$user_address = $request->input('address') ?: 'Adres Belirtilmemiş'; // Fallback if not provided
|
||||
$user_phone = $request->input('phone');
|
||||
|
||||
// Callbacks should ideally be routes we define, but user provided specific URLs.
|
||||
// We will use routes relative to current domain or the ones user specified if they are absolute?
|
||||
// The user script had: "https://www.truncgil.com.tr/pay.php?ok"
|
||||
// I should probably use the current app's URL.
|
||||
$merchant_ok_url = route('payment.success');
|
||||
$merchant_fail_url = route('payment.fail');
|
||||
|
||||
$user_basket = base64_encode(json_encode([
|
||||
['Web / Mobil Yazılım', $payment_amount, 1],
|
||||
]));
|
||||
|
||||
$no_installment = 0;
|
||||
$max_installment = 9;
|
||||
$debug_on = 1;
|
||||
$timeout_limit = 30;
|
||||
|
||||
$hash_str = $merchant_id.$user_ip.$merchant_oid.$email.$payment_amount.$user_basket.$no_installment.$max_installment.$currency.$test_mode;
|
||||
$paytr_token = base64_encode(hash_hmac('sha256', $hash_str.$merchant_salt, $merchant_key, true));
|
||||
|
||||
$post_vals = [
|
||||
'merchant_id' => $merchant_id,
|
||||
'user_ip' => $user_ip,
|
||||
'merchant_oid' => $merchant_oid,
|
||||
'email' => $email,
|
||||
'lang' => 'tr',
|
||||
'payment_amount' => $payment_amount,
|
||||
'paytr_token' => $paytr_token,
|
||||
'user_basket' => $user_basket,
|
||||
'debug_on' => $debug_on,
|
||||
'no_installment' => $no_installment,
|
||||
'max_installment' => $max_installment,
|
||||
'user_name' => $user_name,
|
||||
'user_address' => $user_address,
|
||||
'user_phone' => $user_phone,
|
||||
'merchant_ok_url' => $merchant_ok_url,
|
||||
'merchant_fail_url' => $merchant_fail_url,
|
||||
'timeout_limit' => $timeout_limit,
|
||||
'currency' => $currency,
|
||||
'test_mode' => $test_mode,
|
||||
];
|
||||
|
||||
// Log request for debugging
|
||||
Log::info('PayTR Request', [
|
||||
'merchant_id' => $merchant_id,
|
||||
'merchant_oid' => $merchant_oid,
|
||||
'email' => $email,
|
||||
'payment_amount' => $payment_amount,
|
||||
'user_ip' => $user_ip,
|
||||
]);
|
||||
|
||||
// Curl request
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, 'https://www.paytr.com/odeme/api/get-token');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_vals);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
$result = curl_exec($ch);
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
$curlErrno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlErrno) {
|
||||
Log::error('PayTR cURL error', ['errno' => $curlErrno, 'error' => $curlError]);
|
||||
return back()->with('error', 'PAYTR bağlantı hatası. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
if ($result === false || empty($result)) {
|
||||
Log::error('PayTR returned empty response');
|
||||
return back()->with('error', 'Ödeme sisteminden yanıt alınamadı. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
// Log the raw response for debugging
|
||||
Log::info('PayTR Raw Response', ['response' => $result]);
|
||||
|
||||
$result = json_decode($result, 1);
|
||||
|
||||
// Check if result is valid
|
||||
if (!$result || !is_array($result)) {
|
||||
Log::error('PayTR API returned invalid response', ['response' => $result]);
|
||||
return back()->with('error', 'Ödeme sisteminden geçersiz yanıt alındı. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
if (!isset($result['status'])) {
|
||||
Log::error('PayTR API response missing status', ['response' => $result]);
|
||||
return back()->with('error', 'Ödeme sisteminden beklenmeyen yanıt alındı. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
if ($result['status'] == 'success') {
|
||||
$token = $result['token'];
|
||||
|
||||
// Re-render the online-payment page but with the token
|
||||
// We need to fetch the page content again
|
||||
// Assuming the slug is 'online-odeme' or similar, or we can just pass a dummy page object
|
||||
// if we are lazy, but correct way is to fetch the page associated with the route.
|
||||
// Since we are POSTing here, we don't know the Page model context unless we fetch it.
|
||||
// Let's try to assume 'online-odeme' which is common.
|
||||
|
||||
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
|
||||
if (! $page) {
|
||||
$page = new Page(['title' => 'Online Ödeme', 'content' => '', 'excerpt' => 'Please complete your payment below.']);
|
||||
}
|
||||
|
||||
return view('templates.corporate.online-payment', [
|
||||
'page' => $page,
|
||||
'paytr_token' => $token,
|
||||
]);
|
||||
|
||||
} else {
|
||||
return back()->with('error', 'PAYTR failed. reason:'.$result['reason']);
|
||||
}
|
||||
}
|
||||
|
||||
public function success(Request $request)
|
||||
{
|
||||
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
|
||||
if ($page) {
|
||||
return redirect()->route('page.show', $page->slug)->with('success', 'Your payment has been charged successfully. This will appear on your credit card statement as "PAYTR ODEME HIZMETLERI"');
|
||||
}
|
||||
|
||||
return redirect()->route('home')->with('success', 'Your payment has been charged successfully.');
|
||||
}
|
||||
|
||||
public function fail(Request $request)
|
||||
{
|
||||
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
|
||||
if ($page) {
|
||||
return redirect()->route('page.show', $page->slug)->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
|
||||
}
|
||||
|
||||
return redirect()->route('home')->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Product;
|
||||
use App\Models\Setting;
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
public function show($slug)
|
||||
{
|
||||
$product = Product::where('slug', $slug)
|
||||
->where('is_active', true)
|
||||
->with('category')
|
||||
->firstOrFail();
|
||||
|
||||
// Get Settings
|
||||
$settings = new \stdClass();
|
||||
$allSettings = Setting::where('is_active', true)->get();
|
||||
foreach ($allSettings as $setting) {
|
||||
$settings->{$setting->key} = $setting->value;
|
||||
}
|
||||
|
||||
// --- Header Logic ---
|
||||
$renderedHeader = null;
|
||||
$defaultHeaderId = $settings->default_header ?? null;
|
||||
|
||||
if ($defaultHeaderId) {
|
||||
$headerTemplate = HeaderTemplate::find($defaultHeaderId);
|
||||
if ($headerTemplate) {
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$productData = []; // Ürünlere özel header datası eklenebilir
|
||||
$mergedHeaderData = array_merge($templateDefaults, $productData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$mergedHeaderData,
|
||||
$product // Model context
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Footer Logic ---
|
||||
$renderedFooter = null;
|
||||
$defaultFooterId = $settings->default_footer ?? null;
|
||||
|
||||
if ($defaultFooterId) {
|
||||
$footerTemplate = FooterTemplate::find($defaultFooterId);
|
||||
if ($footerTemplate) {
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$productData = []; // Ürünlere özel footer datası eklenebilir
|
||||
$mergedFooterData = array_merge($templateDefaults, $productData);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$mergedFooterData,
|
||||
$product // Model context
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$meta = [
|
||||
'title' => $product->translate('title'),
|
||||
'description' => \Str::limit(strip_tags($product->translate('content')), 160),
|
||||
'image' => $product->hero_image ? asset('storage/' . $product->hero_image) : null,
|
||||
];
|
||||
|
||||
// Use custom view template if specified
|
||||
if ($product->view_template && view()->exists($product->view_template)) {
|
||||
return view($product->view_template, compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
|
||||
}
|
||||
|
||||
// Use landing page template if landing_page_data exists
|
||||
if (!empty($product->landing_page_data)) {
|
||||
return view('front.products.landing', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
|
||||
}
|
||||
|
||||
// Default view
|
||||
return view('front.products.show', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Proposal;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProposalController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the public proposal presentation page.
|
||||
*/
|
||||
public function show(string $slug)
|
||||
{
|
||||
$proposal = Proposal::where('slug', $slug)->firstOrFail();
|
||||
|
||||
// Increment views count cleanly
|
||||
$proposal->increment('views_count');
|
||||
|
||||
return view('proposals.show', compact('proposal'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle client interaction actions (approve or revision/feedback).
|
||||
*/
|
||||
public function action(Request $request, string $slug)
|
||||
{
|
||||
$proposal = Proposal::where('slug', $slug)->firstOrFail();
|
||||
|
||||
$actionType = $request->input('action_type');
|
||||
|
||||
if ($actionType === 'approve') {
|
||||
$request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
$proposal->update([
|
||||
'status' => 'accepted',
|
||||
'accepted_name' => $request->input('name'),
|
||||
'accepted_at' => now(),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Teklif başarıyla onaylandı! Katılımınız ve iş birliğiniz için teşekkür ederiz.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($actionType === 'feedback') {
|
||||
$request->validate([
|
||||
'feedback' => 'required|string|max:5000',
|
||||
]);
|
||||
|
||||
$proposal->update([
|
||||
'status' => 'revised',
|
||||
'client_feedback' => $request->input('feedback'),
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Revizyon ve görüşleriniz başarıyla iletildi. En kısa sürede güncellenerek sizinle paylaşılacaktır.',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Geçersiz işlem tipi.',
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Page;
|
||||
use App\Models\Blog;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class SitemapController extends Controller
|
||||
{
|
||||
public function index(): Response
|
||||
{
|
||||
$urls = [];
|
||||
|
||||
// 1. Static & Main Pages
|
||||
$urls[] = [
|
||||
'loc' => url('/'),
|
||||
'lastmod' => now()->startOfDay()->toAtomString(),
|
||||
'changefreq' => 'daily',
|
||||
'priority' => '1.0',
|
||||
];
|
||||
|
||||
$urls[] = [
|
||||
'loc' => route('blog.index'),
|
||||
'lastmod' => now()->startOfDay()->toAtomString(),
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.8',
|
||||
];
|
||||
|
||||
$urls[] = [
|
||||
'loc' => route('career.index'),
|
||||
'lastmod' => now()->startOfMonth()->toAtomString(),
|
||||
'changefreq' => 'monthly',
|
||||
'priority' => '0.5',
|
||||
];
|
||||
|
||||
// 2. Dynamic Pages
|
||||
$pages = Page::where('status', 'published')
|
||||
->where('is_homepage', false)
|
||||
->get();
|
||||
foreach ($pages as $page) {
|
||||
$urls[] = [
|
||||
'loc' => url($page->slug),
|
||||
'lastmod' => $page->updated_at->toAtomString(),
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.7',
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Blog Posts
|
||||
if (class_exists(Blog::class)) {
|
||||
$posts = Blog::published()->get();
|
||||
foreach ($posts as $post) {
|
||||
$urls[] = [
|
||||
'loc' => route('blog.show', $post->slug),
|
||||
'lastmod' => $post->updated_at->toAtomString(),
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.6',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Products
|
||||
if (class_exists(Product::class)) {
|
||||
$products = Product::where('is_active', true)->get();
|
||||
foreach ($products as $product) {
|
||||
$urls[] = [
|
||||
'loc' => route('products.show', $product->slug),
|
||||
'lastmod' => $product->updated_at->toAtomString(),
|
||||
'changefreq' => 'weekly',
|
||||
'priority' => '0.7',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return response()->view('sitemap', compact('urls'))
|
||||
->header('Content-Type', 'text/xml');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureSecurityHeaders
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// HSTS (Strict-Transport-Security) - Enforce HTTPS for 1 year
|
||||
// 'includeSubDomains' and 'preload' are optional but often recommended for full security.
|
||||
// A+ score typically looks for this header with a long max-age.
|
||||
if ($request->isSecure()) {
|
||||
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
|
||||
}
|
||||
|
||||
// X-Frame-Options: SAMEORIGIN (prevents clickjacking by allowing framing only by the same site)
|
||||
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
|
||||
|
||||
// X-Content-Type-Options: nosniff (prevents MIME type sniffing)
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// Referrer-Policy: stringent check, usually 'strict-origin-when-cross-origin' is good
|
||||
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// X-XSS-Protection: 1; mode=block (deprecated but still good for older browsers, some scanners check it)
|
||||
$response->headers->set('X-XSS-Protection', '1; mode=block');
|
||||
|
||||
// Permissions-Policy: restrict dangerous features
|
||||
// This is a bit strict, might need adjustment based on site features.
|
||||
// For general sites: geolocation=(), microphone=(), camera=() is often safe.
|
||||
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
||||
|
||||
// Cross-Origin-Opener-Policy: isolate browsing context from cross-origin documents
|
||||
$response->headers->set('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
|
||||
// Content-Security-Policy (CSP) - Start with upgrade-insecure-requests to force HTTPS assets
|
||||
// A full CSP can be tricky and break scripts. 'upgrade-insecure-requests' is safe and good for mixed content.
|
||||
// We can add 'frame-ancestors' here too if X-Frame-Options is ignored by some.
|
||||
// If the user wants A+, a basic CSP is often enough.
|
||||
$csp = "upgrade-insecure-requests; block-all-mixed-content; frame-ancestors 'self';";
|
||||
$response->headers->set('Content-Security-Policy', $csp);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SuperAdminMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => 'Unauthorized'], 403);
|
||||
}
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Award extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'description',
|
||||
'issuer',
|
||||
'award_date',
|
||||
'image',
|
||||
'external_link',
|
||||
'is_featured',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
'category',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'description',
|
||||
'issuer',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'award_date' => 'date',
|
||||
'is_featured' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeFeatured($query)
|
||||
{
|
||||
return $query->where('is_featured', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query
|
||||
->orderBy('sort_order', 'asc')
|
||||
->orderBy('award_date', 'desc');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BlogTopic extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'context',
|
||||
'status',
|
||||
'last_used_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'last_used_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CareerApplication extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'phone',
|
||||
'type',
|
||||
'cv_path',
|
||||
'nda_path',
|
||||
'contract_path',
|
||||
'id_photocopy_path',
|
||||
'git_knowledge',
|
||||
'ai_usage',
|
||||
'message',
|
||||
'status',
|
||||
'username',
|
||||
'password',
|
||||
'signed_internship_form_path',
|
||||
'to_be_signed_internship_form_path',
|
||||
'internship_start_date',
|
||||
'internship_end_date',
|
||||
'internship_total_days',
|
||||
'github_repo',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class CompanyHistoryItem extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
public const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4'];
|
||||
|
||||
protected $fillable = [
|
||||
'year',
|
||||
'quarter',
|
||||
'title',
|
||||
'content',
|
||||
'color',
|
||||
'icon',
|
||||
'position',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'year' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query
|
||||
->orderBy('year', 'asc')
|
||||
->orderByRaw("CASE quarter WHEN 'Q1' THEN 1 WHEN 'Q2' THEN 2 WHEN 'Q3' THEN 3 WHEN 'Q4' THEN 4 ELSE 5 END")
|
||||
->orderBy('sort_order', 'asc');
|
||||
}
|
||||
|
||||
public function getResolvedPositionAttribute(): string
|
||||
{
|
||||
if (in_array($this->position, ['left', 'right'], true)) {
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
return ($this->sort_order % 2 === 1) ? 'right' : 'left';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class MusicProduction extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $table = 'music_productions';
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'spotify_album_id',
|
||||
'spotify_url',
|
||||
'spotify_cover_url',
|
||||
'spotify_type',
|
||||
'spotify_synced_at',
|
||||
'youtube_video_id',
|
||||
'youtube_url',
|
||||
'youtube_cover_url',
|
||||
'youtube_description',
|
||||
'youtube_synced_at',
|
||||
'cover_image',
|
||||
'content',
|
||||
'client_name',
|
||||
'production_date',
|
||||
'gallery',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
/**
|
||||
* Translatable fields
|
||||
*/
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
'client_name',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'production_date' => 'date',
|
||||
'spotify_synced_at' => 'datetime',
|
||||
'youtube_synced_at' => 'datetime',
|
||||
'gallery' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
return route('music-productions.show', $this->slug);
|
||||
}
|
||||
|
||||
public function getCoverImageUrlAttribute()
|
||||
{
|
||||
if ($this->cover_image) {
|
||||
return asset('storage/' . $this->cover_image);
|
||||
}
|
||||
|
||||
if ($this->spotify_cover_url) {
|
||||
return $this->spotify_cover_url;
|
||||
}
|
||||
|
||||
if ($this->youtube_cover_url) {
|
||||
return $this->youtube_cover_url;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getDisplayCoverImageAttribute(): ?string
|
||||
{
|
||||
if ($this->cover_image) {
|
||||
return $this->cover_image;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
|
||||
public function scopeOrdered($query)
|
||||
{
|
||||
return $query->orderBy('sort_order', 'asc')->orderBy('production_date', 'desc');
|
||||
}
|
||||
}
|
||||
+15
-6
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\SectionTemplate;
|
||||
use App\Support\PageTemplateHero;
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -34,8 +35,10 @@ class Page extends Model
|
||||
// Template System
|
||||
'header_template_id',
|
||||
'header_data',
|
||||
'custom_header_blade',
|
||||
'footer_template_id',
|
||||
'footer_data',
|
||||
'custom_footer_blade',
|
||||
'sections_data',
|
||||
];
|
||||
|
||||
@@ -78,6 +81,11 @@ class Page extends Model
|
||||
return $this->hasMany(Page::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function testimonials()
|
||||
{
|
||||
return $this->hasMany(Testimonial::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header template for the page.
|
||||
*/
|
||||
@@ -108,13 +116,14 @@ class Page extends Model
|
||||
return '/' . $this->slug;
|
||||
}
|
||||
|
||||
public function getFeaturedImageUrlAttribute()
|
||||
public function getFeaturedImageUrlAttribute(): ?string
|
||||
{
|
||||
if ($this->featured_image) {
|
||||
return asset('storage/' . $this->featured_image);
|
||||
}
|
||||
|
||||
return null;
|
||||
return PageTemplateHero::urlForPage($this);
|
||||
}
|
||||
|
||||
public function usesTemplateHeroImage(): bool
|
||||
{
|
||||
return ! $this->featured_image && PageTemplateHero::hasHero($this->template);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\HasTranslations;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'type',
|
||||
'product_category_id',
|
||||
'hero_image',
|
||||
'content',
|
||||
'view_template',
|
||||
'external_url',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
'landing_page_data',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'title' => 'array',
|
||||
'content' => 'array',
|
||||
'landing_page_data' => 'array',
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::saving(function ($model) {
|
||||
// Eğer title boşsa ve translations içinden bir title geliyorsa onu ata
|
||||
if (empty($model->title)) {
|
||||
$translations = request()->input('translations');
|
||||
$defaultLocale = app()->getLocale();
|
||||
|
||||
if (is_array($translations)) {
|
||||
if (!empty($translations[$defaultLocale]['title'])) {
|
||||
$model->title = $translations[$defaultLocale]['title'];
|
||||
} else {
|
||||
foreach ($translations as $locale => $fields) {
|
||||
if (!empty($fields['title'])) {
|
||||
$model->title = $fields['title'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($model->title)) {
|
||||
$model->title = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class, 'product_category_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\HasTranslations;
|
||||
|
||||
class ProductCategory extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'parent_id',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'title' => 'array',
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::saving(function ($model) {
|
||||
// Eğer title boşsa ve translations içinden bir title geliyorsa onu ata
|
||||
if (empty($model->title)) {
|
||||
$translations = request()->input('translations');
|
||||
$defaultLocale = app()->getLocale();
|
||||
|
||||
if (is_array($translations)) {
|
||||
// Önce varsayılan dildeki çeviriye bak
|
||||
if (!empty($translations[$defaultLocale]['title'])) {
|
||||
$model->title = $translations[$defaultLocale]['title'];
|
||||
}
|
||||
// Yoksa ilk bulduğu dolu çeviriyi al
|
||||
else {
|
||||
foreach ($translations as $locale => $fields) {
|
||||
if (!empty($fields['title'])) {
|
||||
$model->title = $fields['title'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hala boşsa boş bir array veya string ata (veritabanı hatasını önlemek için)
|
||||
if (empty($model->title)) {
|
||||
$model->title = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->hasMany(Product::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Proposal extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'slug',
|
||||
'title',
|
||||
'client_name',
|
||||
'client_email',
|
||||
'content',
|
||||
'total_price',
|
||||
'currency',
|
||||
'status',
|
||||
'valid_until',
|
||||
'client_feedback',
|
||||
'accepted_name',
|
||||
'accepted_at',
|
||||
'views_count',
|
||||
'meta',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'valid_until' => 'date',
|
||||
'accepted_at' => 'datetime',
|
||||
'meta' => 'array',
|
||||
'views_count' => 'integer',
|
||||
'total_price' => 'decimal:2',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($proposal) {
|
||||
if (empty($proposal->uuid)) {
|
||||
$proposal->uuid = (string) Str::uuid();
|
||||
}
|
||||
if (empty($proposal->created_by) && auth()->check()) {
|
||||
$proposal->created_by = auth()->id();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created the proposal.
|
||||
*/
|
||||
public function creator()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public url for the proposal.
|
||||
*/
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
return route('proposals.show', $this->slug);
|
||||
}
|
||||
}
|
||||
+135
-13
@@ -10,6 +10,17 @@ class Setting extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::saved(function ($setting) {
|
||||
\Illuminate\Support\Facades\Cache::forget("app_setting_{$setting->key}");
|
||||
});
|
||||
|
||||
static::deleted(function ($setting) {
|
||||
\Illuminate\Support\Facades\Cache::forget("app_setting_{$setting->key}");
|
||||
});
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'value',
|
||||
@@ -64,17 +75,115 @@ class Setting extends Model
|
||||
$this->attributes['value'] = $value ? '1' : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value from different field names
|
||||
*/
|
||||
public function setValueFileAttribute($value)
|
||||
{
|
||||
// Filament bazen dosya yolunu array olarak döndürebilir, bu durumda ilk elemanı alıyoruz
|
||||
if (is_array($value)) {
|
||||
$this->attributes['value'] = array_values($value)[0] ?? null;
|
||||
$filePath = array_values($value)[0] ?? null;
|
||||
} else {
|
||||
$this->attributes['value'] = $value;
|
||||
$filePath = $value;
|
||||
}
|
||||
|
||||
if ($filePath && $this->key === 'hero_bg') {
|
||||
$filePath = $this->processHeroBackground($filePath);
|
||||
}
|
||||
|
||||
$this->attributes['value'] = $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the hero background image: convert to webp, resize if necessary, and compress.
|
||||
*/
|
||||
protected function processHeroBackground($filePath)
|
||||
{
|
||||
try {
|
||||
$disk = \Illuminate\Support\Facades\Storage::disk('public');
|
||||
if (!$disk->exists($filePath)) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
$absolutePath = $disk->path($filePath);
|
||||
|
||||
// Get image info
|
||||
$imageInfo = @getimagesize($absolutePath);
|
||||
if (!$imageInfo) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
$mimeType = $imageInfo['mime'];
|
||||
|
||||
// Create GD image resource based on mime type
|
||||
switch ($mimeType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
$image = @imagecreatefromjpeg($absolutePath);
|
||||
break;
|
||||
case 'image/png':
|
||||
$image = @imagecreatefrompng($absolutePath);
|
||||
break;
|
||||
case 'image/webp':
|
||||
$image = @imagecreatefromwebp($absolutePath);
|
||||
break;
|
||||
case 'image/gif':
|
||||
$image = @imagecreatefromgif($absolutePath);
|
||||
break;
|
||||
default:
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
if (!$image) {
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
// Preserve transparency or handle it for webp if needed
|
||||
imagealphablending($image, true);
|
||||
imagesavealpha($image, true);
|
||||
|
||||
$originalWidth = imagesx($image);
|
||||
$originalHeight = imagesy($image);
|
||||
|
||||
// Target dimensions: Hero bg size, max width 1920px
|
||||
$maxWidth = 1920;
|
||||
if ($originalWidth > $maxWidth) {
|
||||
$newWidth = $maxWidth;
|
||||
$newHeight = (int) (($originalHeight / $originalWidth) * $maxWidth);
|
||||
|
||||
$resizedImage = imagecreatetruecolor($newWidth, $newHeight);
|
||||
|
||||
// Preserve transparency
|
||||
imagealphablending($resizedImage, false);
|
||||
imagesavealpha($resizedImage, true);
|
||||
|
||||
// Resize
|
||||
imagecopyresampled($resizedImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $originalWidth, $originalHeight);
|
||||
imagedestroy($image);
|
||||
$image = $resizedImage;
|
||||
}
|
||||
|
||||
// Generate new webp path
|
||||
$pathInfo = pathinfo($filePath);
|
||||
$newWebpPath = $pathInfo['dirname'] . '/' . $pathInfo['filename'] . '.webp';
|
||||
$absoluteWebpPath = $disk->path($newWebpPath);
|
||||
|
||||
// Save as webp with 80% quality (excellent quality-to-size ratio)
|
||||
if (imagewebp($image, $absoluteWebpPath, 80)) {
|
||||
imagedestroy($image);
|
||||
|
||||
// If the file extension changed, delete the original file
|
||||
if ($newWebpPath !== $filePath) {
|
||||
$disk->delete($filePath);
|
||||
}
|
||||
|
||||
return $newWebpPath;
|
||||
}
|
||||
|
||||
imagedestroy($image);
|
||||
} catch (\Exception $e) {
|
||||
// Log error or fallback to original
|
||||
\Illuminate\Support\Facades\Log::error('Hero background image processing failed: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return $filePath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -309,15 +418,24 @@ class Setting extends Model
|
||||
return isset($this->attributes['value']) ? json_decode($this->attributes['value'], true) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an active setting record by key.
|
||||
*/
|
||||
public static function findActiveByKey(string $key): ?self
|
||||
{
|
||||
return static::query()
|
||||
->where('key', $key)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get setting by key
|
||||
*/
|
||||
public static function get(string $key, $default = null)
|
||||
{
|
||||
$setting = static::where('key', $key)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
$setting = static::findActiveByKey($key);
|
||||
|
||||
if (!$setting) {
|
||||
return $default;
|
||||
}
|
||||
@@ -369,12 +487,16 @@ class Setting extends Model
|
||||
*/
|
||||
public static function getGroup(string $group): array
|
||||
{
|
||||
return static::where('group', $group)
|
||||
return static::query()
|
||||
->where('group', $group)
|
||||
->where('is_active', true)
|
||||
->pluck('value', 'key')
|
||||
->map(function ($value, $key) use ($group) {
|
||||
$setting = static::where('key', $key)->first();
|
||||
return static::castValue($value, $setting->type);
|
||||
->map(function ($value, $key) {
|
||||
$setting = static::findActiveByKey($key);
|
||||
|
||||
return $setting
|
||||
? static::castValue($value, $setting->type)
|
||||
: $value;
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SiteTranslation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'hash',
|
||||
'key',
|
||||
'translations',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'translations' => 'array',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->hash)) {
|
||||
$model->hash = md5($model->key);
|
||||
}
|
||||
});
|
||||
|
||||
static::updating(function ($model) {
|
||||
// Key değiştiyse hash'i güncelle
|
||||
if ($model->isDirty('key')) {
|
||||
$model->hash = md5($model->key);
|
||||
}
|
||||
});
|
||||
|
||||
static::saved(function ($model) {
|
||||
Cache::forget("site_translation_{$model->hash}");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation for a specific key
|
||||
*/
|
||||
public static function getTranslation(string $key): string
|
||||
{
|
||||
$hash = md5($key);
|
||||
$locale = app()->getLocale();
|
||||
|
||||
// Cache'ten veya DB'den al
|
||||
// Cache süresi: 24 saat
|
||||
$translation = Cache::remember("site_translation_{$hash}", 60 * 60 * 24, function () use ($hash, $key) {
|
||||
return static::firstOrCreate(
|
||||
['hash' => $hash],
|
||||
['key' => $key]
|
||||
);
|
||||
});
|
||||
|
||||
// Eğer kayıt yeni oluşturulduysa ve key eksikse (concurrency nedeniyle), key'i set et
|
||||
if ($translation->wasRecentlyCreated && empty($translation->translations)) {
|
||||
// Yeni oluşturuldu, henüz çeviri yok.
|
||||
// Varsayılan olarak key'i döndür.
|
||||
return $key;
|
||||
}
|
||||
|
||||
$translations = $translation->translations ?? [];
|
||||
|
||||
if (isset($translations[$locale]) && !empty($translations[$locale])) {
|
||||
return $translations[$locale];
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Testimonial extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'page_id',
|
||||
'name',
|
||||
'position',
|
||||
'content',
|
||||
'rating',
|
||||
'avatar',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user