Compare commits
336 Commits
main
...
04e62ea9b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 04e62ea9b8 | |||
| 66366aa8b8 | |||
| 0e558fd1dc | |||
| 01be250e6c | |||
| 6614d13be2 | |||
| 61480399b3 | |||
| e3e7ac7f18 | |||
| f1c7e57e43 | |||
| ad50102c42 | |||
| 007da1227d | |||
| 58ae406b36 | |||
| 794f9556de | |||
| 6be7f4442a | |||
| 9361a01c80 | |||
| f9abe3c02a | |||
| efc6b911e7 | |||
| 793a843be7 | |||
| 742303eb73 | |||
| cfce7b8b3f | |||
| 13d12e4f8f | |||
| ba6431e500 | |||
| eca57bf9cc | |||
| 89b81e6ee8 | |||
| fa06ba7545 | |||
| 4eb0a96832 | |||
| 3600cc2334 | |||
| 554f22695d | |||
| af82f47ea0 | |||
| 612a24042b | |||
| 082fb33af3 | |||
| 5b07f385b1 | |||
| aa4a4b3e27 | |||
| b776ebbd26 | |||
| 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
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
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
|
# 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',
|
'span' => 'text.content',
|
||||||
'address' => 'text.address',
|
'address' => 'text.address',
|
||||||
'button' => 'text.button',
|
'button' => 'text.button',
|
||||||
'a' => 'url.link',
|
'a' => 'text.link_text',
|
||||||
'li' => 'text.list_item',
|
'li' => 'text.list_item',
|
||||||
'label' => 'text.label',
|
'label' => 'text.label',
|
||||||
];
|
];
|
||||||
@@ -260,7 +260,7 @@ class ImportHtmlTemplates extends Command
|
|||||||
$links = $xpath->query('.//a', $node);
|
$links = $xpath->query('.//a', $node);
|
||||||
foreach ($links as $link) {
|
foreach ($links as $link) {
|
||||||
$href = $link->getAttribute('href');
|
$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);
|
$fixedHref = $this->fixAssetPath($href);
|
||||||
if (str_ends_with($fixedHref, '.html')) {
|
if (str_ends_with($fixedHref, '.html')) {
|
||||||
@@ -268,7 +268,7 @@ class ImportHtmlTemplates extends Command
|
|||||||
if($fixedHref == 'index') $fixedHref = '/';
|
if($fixedHref == 'index') $fixedHref = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
$key = $this->generateKey('url.link', $counts);
|
$key = $this->generateKey('text.link_url', $counts);
|
||||||
$data[$key] = $fixedHref;
|
$data[$key] = $fixedHref;
|
||||||
$link->setAttribute('href', "{" . $key . "}");
|
$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 $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';
|
protected static UnitEnum|string|null $navigationGroup = 'Blog';
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class BlogResource extends Resource
|
|||||||
{
|
{
|
||||||
protected static ?string $model = Blog::class;
|
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
|
public static function getNavigationLabel(): string
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,372 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\InternApplications;
|
||||||
|
|
||||||
|
use App\Models\CareerApplication;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
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\MarkdownEditor;
|
||||||
|
use Filament\Schemas\Components\Tabs;
|
||||||
|
use Filament\Schemas\Components\Tabs\Tab;
|
||||||
|
use Filament\Schemas\Components\Livewire;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Filament\Schemas\Components\Utilities\Set;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class InternApplicationResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = CareerApplication::class;
|
||||||
|
|
||||||
|
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-academic-cap';
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('career.internship', ['default' => 'Staj Başvurusu']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('career.internship_title', ['default' => 'Staj Başvuruları']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getEloquentQuery(): Builder
|
||||||
|
{
|
||||||
|
return parent::getEloquentQuery()->where('type', 'internship');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(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()
|
||||||
|
->formatStateUsing(fn () => null)
|
||||||
|
->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(
|
||||||
|
\Filament\Actions\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()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(function ($state, Set $set) {
|
||||||
|
if ($state) {
|
||||||
|
$set('status', 'accepted');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->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(),
|
||||||
|
|
||||||
|
Livewire::make(\App\Livewire\InternJournalTimeline::class)
|
||||||
|
->columnSpanFull()
|
||||||
|
]),
|
||||||
|
|
||||||
|
Tab::make('Sertifika & Transkript')
|
||||||
|
->icon('heroicon-o-academic-cap')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('certificate_code')
|
||||||
|
->label('Doğrulama Kodu')
|
||||||
|
->helperText('Belge kaydedildiğinde benzersiz doğrulama kodu otomatik olarak üretilir.')
|
||||||
|
->readonly()
|
||||||
|
->nullable(),
|
||||||
|
|
||||||
|
MarkdownEditor::make('transcript_markdown')
|
||||||
|
->label('Akademik Transkript (Markdown)')
|
||||||
|
->columnSpanFull()
|
||||||
|
->default(function () {
|
||||||
|
return "### STAJ AKADEMİK TRANSKRİPTİ VE PERFORMANS RAPORU\n\n" .
|
||||||
|
"#### 🛠️ Deneyimlenen Teknolojiler ve Kazanımlar\n" .
|
||||||
|
"| Modül / Çalışma Alanı | Kullanılan Teknolojiler / Araçlar | Değerlendirme |\n" .
|
||||||
|
"| --- | --- | --- |\n" .
|
||||||
|
"| Backend Mimari & API | Laravel framework, RESTful API, MySQL | Başarılı |\n" .
|
||||||
|
"| Arayüz & UI/UX Uygulamaları | Flutter, CSS, Glassmorphic Tasarım Prensipleri | Üstün Başarı |\n" .
|
||||||
|
"| Masaüstü & Sistem Entegrasyonu | Electron.js, Git / GitHub | Başarılı |\n" .
|
||||||
|
"| Takım Çalışması & Proje Yönetimi | Agile / Scrum, Slack, JIRA | Başarılı |\n\n" .
|
||||||
|
"#### 📊 Performans Değerlendirme Kriterleri\n" .
|
||||||
|
"| Değerlendirme Kriteri | Puan (100 Üzerinden) | Harf Notu |\n" .
|
||||||
|
"| --- | --- | --- |\n" .
|
||||||
|
"| Teknik Sorumluluk ve Görev Bilinci | 95 | AA |\n" .
|
||||||
|
"| Problem Çözme ve Analitik Düşünme | 90 | BA |\n" .
|
||||||
|
"| Ekip Çalışması ve İletişim Uyum | 95 | AA |\n" .
|
||||||
|
"| Öğrenme Hızı ve Adaptasyon | 100 | AA |\n" .
|
||||||
|
"| **GENEL BAŞARI ORTALAMASI** | **95.00** | **AA (Mükemmel)** |\n\n" .
|
||||||
|
"#### 📝 Danışman Görüşü ve Değerlendirme Notu\n" .
|
||||||
|
"\"Stajyerimiz, staj süresi boyunca kendisine verilen görevleri büyük bir titizlikle yerine getirmiştir. Özellikle karşılaştığı teknik problemlere getirdiği pratik çözümler ve yeni teknolojileri öğrenme isteği takdir edilmeye değerdir. Kurumumuz bünyesinde yürüttüğümüz projelere sağladığı katkılardan ötürü teşekkür eder, profesyonel kariyerinde başarılar dileriz.\"";
|
||||||
|
}),
|
||||||
|
])->columns(1)
|
||||||
|
])->columnSpanFull()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(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('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('certificate_code')
|
||||||
|
->label('Sertifika Kodu')
|
||||||
|
->searchable()
|
||||||
|
->placeholder('Yok'),
|
||||||
|
|
||||||
|
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'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->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('view_certificate')
|
||||||
|
->label('Sertifika Doğrulama')
|
||||||
|
->icon('heroicon-o-academic-cap')
|
||||||
|
->color('success')
|
||||||
|
->url(fn ($record) => $record->certificate_code ? route('internship.verify', $record->certificate_code) : null)
|
||||||
|
->visible(fn ($record) => !empty($record->certificate_code))
|
||||||
|
->openUrlInNewTab(),
|
||||||
|
DeleteAction::make(),
|
||||||
|
])
|
||||||
|
->bulkActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->defaultSort('created_at', 'desc');
|
||||||
|
}
|
||||||
|
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListInternApplications::route('/'),
|
||||||
|
'create' => Pages\CreateInternApplication::route('/create'),
|
||||||
|
'edit' => Pages\EditInternApplication::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getWidgets(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Widgets\InternGanttChartWidget::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\InternApplications\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateInternApplication extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = InternApplicationResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\InternApplications\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditInternApplication extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = InternApplicationResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\InternApplications\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Admin\Resources\InternApplications\InternApplicationResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListInternApplications extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = InternApplicationResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderWidgets(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
\App\Filament\Admin\Resources\InternApplications\Widgets\InternGanttChartWidget::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\InternApplications\Widgets;
|
||||||
|
|
||||||
|
use App\Models\CareerApplication;
|
||||||
|
use Filament\Widgets\Widget;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class InternGanttChartWidget extends Widget
|
||||||
|
{
|
||||||
|
protected string $view = 'filament.admin.resources.intern-applications.widgets.intern-gantt-chart-widget';
|
||||||
|
|
||||||
|
protected int | string | array $columnSpan = 'full';
|
||||||
|
|
||||||
|
public function getViewData(): array
|
||||||
|
{
|
||||||
|
$interns = CareerApplication::query()
|
||||||
|
->where('type', 'internship')
|
||||||
|
->where('status', 'accepted')
|
||||||
|
->whereNotNull('internship_start_date')
|
||||||
|
->whereNotNull('internship_end_date')
|
||||||
|
->orderBy('internship_start_date', 'asc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($interns->isEmpty()) {
|
||||||
|
return [
|
||||||
|
'interns' => [],
|
||||||
|
'months' => [],
|
||||||
|
'totalDays' => 0,
|
||||||
|
'minDate' => null,
|
||||||
|
'todayPercent' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$minDate = Carbon::parse($interns->min('internship_start_date'))->startOfDay();
|
||||||
|
$maxDate = Carbon::parse($interns->max('internship_end_date'))->endOfDay();
|
||||||
|
|
||||||
|
// Pad the range slightly for better visual presentation
|
||||||
|
$minDate = $minDate->copy()->subDays(5);
|
||||||
|
$maxDate = $maxDate->copy()->addDays(5);
|
||||||
|
|
||||||
|
$totalDays = $minDate->diffInDays($maxDate);
|
||||||
|
if ($totalDays <= 0) {
|
||||||
|
$totalDays = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate months in range
|
||||||
|
$months = [];
|
||||||
|
$tempDate = $minDate->copy()->startOfMonth();
|
||||||
|
$endLimit = $maxDate->copy()->endOfMonth();
|
||||||
|
|
||||||
|
while ($tempDate->lte($endLimit)) {
|
||||||
|
$monthStart = $tempDate->copy()->startOfMonth();
|
||||||
|
$monthEnd = $tempDate->copy()->endOfMonth();
|
||||||
|
|
||||||
|
$overlapStart = $monthStart->gt($minDate) ? $monthStart : $minDate;
|
||||||
|
$overlapEnd = $monthEnd->lt($maxDate) ? $monthEnd : $maxDate;
|
||||||
|
|
||||||
|
if ($overlapStart->lte($overlapEnd)) {
|
||||||
|
$daysInOverlap = $overlapStart->diffInDays($overlapEnd) + 1;
|
||||||
|
$startOffset = $minDate->diffInDays($overlapStart);
|
||||||
|
$months[] = [
|
||||||
|
'name' => $tempDate->translatedFormat('F Y'),
|
||||||
|
'left' => ($startOffset / $totalDays) * 100,
|
||||||
|
'width' => ($daysInOverlap / $totalDays) * 100,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$tempDate->addMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate today's line position if it falls within the range
|
||||||
|
$today = Carbon::today();
|
||||||
|
$todayPercent = null;
|
||||||
|
if ($today->between($minDate, $maxDate)) {
|
||||||
|
$todayPercent = ($minDate->diffInDays($today) / $totalDays) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
$formattedInterns = $interns->map(function ($intern) use ($minDate, $totalDays) {
|
||||||
|
$start = Carbon::parse($intern->internship_start_date)->startOfDay();
|
||||||
|
$end = Carbon::parse($intern->internship_end_date)->endOfDay();
|
||||||
|
|
||||||
|
$startOffset = $minDate->diffInDays($start);
|
||||||
|
$duration = $start->diffInDays($end) + 1;
|
||||||
|
|
||||||
|
$left = ($startOffset / $totalDays) * 100;
|
||||||
|
$width = ($duration / $totalDays) * 100;
|
||||||
|
|
||||||
|
// Ensure width is at least a visible sliver
|
||||||
|
if ($width < 1.5) {
|
||||||
|
$width = 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign a stable color palette based on the intern ID
|
||||||
|
$palettes = [
|
||||||
|
[
|
||||||
|
'bg' => 'bg-emerald-500 dark:bg-emerald-600',
|
||||||
|
'gradient' => 'from-emerald-400 to-teal-500 dark:from-emerald-500 dark:to-teal-600',
|
||||||
|
'text' => 'text-emerald-950 dark:text-emerald-50',
|
||||||
|
'border' => 'border-emerald-500/20',
|
||||||
|
'shadow' => 'shadow-emerald-500/20'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bg' => 'bg-blue-500 dark:bg-blue-600',
|
||||||
|
'gradient' => 'from-blue-400 to-indigo-500 dark:from-blue-500 dark:to-indigo-600',
|
||||||
|
'text' => 'text-blue-950 dark:text-blue-50',
|
||||||
|
'border' => 'border-blue-500/20',
|
||||||
|
'shadow' => 'shadow-blue-500/20'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bg' => 'bg-rose-500 dark:bg-rose-600',
|
||||||
|
'gradient' => 'from-rose-400 to-pink-500 dark:from-rose-500 dark:to-pink-600',
|
||||||
|
'text' => 'text-rose-950 dark:text-rose-50',
|
||||||
|
'border' => 'border-rose-500/20',
|
||||||
|
'shadow' => 'shadow-rose-500/20'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bg' => 'bg-amber-500 dark:bg-amber-600',
|
||||||
|
'gradient' => 'from-amber-400 to-orange-500 dark:from-amber-500 dark:to-orange-600',
|
||||||
|
'text' => 'text-amber-950 dark:text-amber-50',
|
||||||
|
'border' => 'border-amber-500/20',
|
||||||
|
'shadow' => 'shadow-amber-500/20'
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'bg' => 'bg-violet-500 dark:bg-violet-600',
|
||||||
|
'gradient' => 'from-violet-400 to-purple-500 dark:from-violet-500 dark:to-purple-600',
|
||||||
|
'text' => 'text-violet-950 dark:text-violet-50',
|
||||||
|
'border' => 'border-violet-500/20',
|
||||||
|
'shadow' => 'shadow-violet-500/20'
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$paletteIndex = $intern->id % count($palettes);
|
||||||
|
$color = $palettes[$paletteIndex];
|
||||||
|
|
||||||
|
$startMonth = (int) $start->format('n');
|
||||||
|
$endMonth = (int) $end->format('n');
|
||||||
|
$gridStart = $startMonth + 1;
|
||||||
|
$gridEnd = min(14, $endMonth + 2);
|
||||||
|
|
||||||
|
// Background color map based on user's examples
|
||||||
|
$hexColors = [
|
||||||
|
'#2ecaac', // Emerald Teal
|
||||||
|
'#54c6f9', // Sky Blue
|
||||||
|
'#ff6252', // Red Coral
|
||||||
|
'#f59e0b', // Amber
|
||||||
|
'#8b5cf6', // Violet
|
||||||
|
];
|
||||||
|
$hexColor = $hexColors[$intern->id % count($hexColors)];
|
||||||
|
$isStripes = ($intern->id % 2 === 0);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $intern->id,
|
||||||
|
'name' => $intern->name,
|
||||||
|
'start_date' => $start->translatedFormat('d M Y'),
|
||||||
|
'end_date' => $end->translatedFormat('d M Y'),
|
||||||
|
'total_days' => $intern->internship_total_days,
|
||||||
|
'left' => $left,
|
||||||
|
'width' => $width,
|
||||||
|
'color' => $color,
|
||||||
|
'grid_start' => $gridStart,
|
||||||
|
'grid_end' => $gridEnd,
|
||||||
|
'hex_color' => $hexColor,
|
||||||
|
'is_stripes' => $isStripes,
|
||||||
|
'raw_start' => $intern->internship_start_date,
|
||||||
|
'raw_end' => $intern->internship_end_date,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
'interns' => $formattedInterns,
|
||||||
|
'months' => $months,
|
||||||
|
'todayPercent' => $todayPercent,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\JobApplications;
|
||||||
|
|
||||||
|
use App\Models\CareerApplication;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
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\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
class JobApplicationResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = CareerApplication::class;
|
||||||
|
|
||||||
|
protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-briefcase';
|
||||||
|
|
||||||
|
public static function getNavigationLabel(): string
|
||||||
|
{
|
||||||
|
return __('career.job_application_title', ['default' => 'İş Başvuruları']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('career.job', ['default' => 'İş Başvurusu']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPluralModelLabel(): string
|
||||||
|
{
|
||||||
|
return __('career.job_application_title', ['default' => 'İş Başvuruları']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getEloquentQuery(): Builder
|
||||||
|
{
|
||||||
|
return parent::getEloquentQuery()->where('type', 'job');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
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(),
|
||||||
|
|
||||||
|
FileUpload::make('cv_path')
|
||||||
|
->label(__('career.cv'))
|
||||||
|
->disk('public')
|
||||||
|
->directory('cvs')
|
||||||
|
->required()
|
||||||
|
->disabled()
|
||||||
|
->downloadable(),
|
||||||
|
|
||||||
|
FileUpload::make('nda_path')
|
||||||
|
->label(__('career.nda'))
|
||||||
|
->disk('public')
|
||||||
|
->directory('ndas')
|
||||||
|
->disabled()
|
||||||
|
->downloadable(),
|
||||||
|
|
||||||
|
FileUpload::make('contract_path')
|
||||||
|
->label(__('career.contract'))
|
||||||
|
->disk('public')
|
||||||
|
->directory('contracts')
|
||||||
|
->disabled()
|
||||||
|
->downloadable(),
|
||||||
|
|
||||||
|
FileUpload::make('id_photocopy_path')
|
||||||
|
->label(__('career.id_photocopy', ['default' => 'Kimlik Fotokopisi']))
|
||||||
|
->disk('public')
|
||||||
|
->directory('id_photocopies')
|
||||||
|
->disabled()
|
||||||
|
->downloadable(),
|
||||||
|
|
||||||
|
Toggle::make('git_knowledge')
|
||||||
|
->label(__('career.git_knowledge'))
|
||||||
|
->disabled(),
|
||||||
|
|
||||||
|
Toggle::make('ai_usage')
|
||||||
|
->label(__('career.ai_usage'))
|
||||||
|
->disabled(),
|
||||||
|
|
||||||
|
Textarea::make('message')
|
||||||
|
->label(__('career.message'))
|
||||||
|
->disabled()
|
||||||
|
->columnSpanFull(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(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('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'),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->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_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');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'index' => Pages\ListJobApplications::route('/'),
|
||||||
|
'create' => Pages\CreateJobApplication::route('/create'),
|
||||||
|
'edit' => Pages\EditJobApplication::route('/{record}/edit'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\JobApplications\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateJobApplication extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = JobApplicationResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\JobApplications\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditJobApplication extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = JobApplicationResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
DeleteAction::make(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Admin\Resources\JobApplications\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Admin\Resources\JobApplications\JobApplicationResource;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListJobApplications extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = JobApplicationResource::class;
|
||||||
|
}
|
||||||
@@ -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
|
public static function getRelations(): array
|
||||||
{
|
{
|
||||||
return [
|
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\SectionTemplatesSection;
|
||||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection;
|
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\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;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
@@ -49,6 +52,11 @@ class PageForm
|
|||||||
SectionTemplatesSection::make(),
|
SectionTemplatesSection::make(),
|
||||||
FooterTemplateSection::make(),
|
FooterTemplateSection::make(),
|
||||||
|
|
||||||
|
// Full Width - Logos (Only for Logolarımız template)
|
||||||
|
LogosSection::make(),
|
||||||
|
PartnersSection::make(),
|
||||||
|
BankAccountsSection::make(),
|
||||||
|
|
||||||
// Full Width - Translations
|
// Full Width - Translations
|
||||||
TranslationsSection::make(),
|
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')
|
RichEditor::make('content')
|
||||||
->label(__('pages.content_field'))
|
->label(__('pages.content_field'))
|
||||||
->required()
|
|
||||||
->fileAttachmentsDisk('public')
|
->fileAttachmentsDisk('public')
|
||||||
->fileAttachmentsDirectory('pages')
|
->fileAttachmentsDirectory('pages')
|
||||||
->fileAttachmentsVisibility('public')
|
->fileAttachmentsVisibility('public')
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
|
|||||||
use Filament\Schemas\Components\Utilities\Get;
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
use Filament\Schemas\Components\Utilities\Set;
|
use Filament\Schemas\Components\Utilities\Set;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
class FooterTemplateSection
|
class FooterTemplateSection
|
||||||
{
|
{
|
||||||
public static function make(): Section
|
public static function make(): Section
|
||||||
@@ -17,9 +19,35 @@ class FooterTemplateSection
|
|||||||
return Section::make(__('pages.form_section_footer_template'))
|
return Section::make(__('pages.form_section_footer_template'))
|
||||||
->description(__('pages.form_section_footer_template_desc'))
|
->description(__('pages.form_section_footer_template_desc'))
|
||||||
->schema([
|
->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')
|
Select::make('footer_template_id')
|
||||||
->label(__('pages.footer_template_field'))
|
->label(__('pages.footer_template_field'))
|
||||||
->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
|
->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||||
|
->visible(fn (Get $get) => empty($get('custom_footer_blade')))
|
||||||
->searchable()
|
->searchable()
|
||||||
->live()
|
->live()
|
||||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
->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\Get;
|
||||||
use Filament\Schemas\Components\Utilities\Set;
|
use Filament\Schemas\Components\Utilities\Set;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
|
||||||
class HeaderTemplateSection
|
class HeaderTemplateSection
|
||||||
{
|
{
|
||||||
public static function make(): Section
|
public static function make(): Section
|
||||||
@@ -17,9 +19,35 @@ class HeaderTemplateSection
|
|||||||
return Section::make(__('pages.form_section_header_template'))
|
return Section::make(__('pages.form_section_header_template'))
|
||||||
->description(__('pages.form_section_header_template_desc'))
|
->description(__('pages.form_section_header_template_desc'))
|
||||||
->schema([
|
->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')
|
Select::make('header_template_id')
|
||||||
->label(__('pages.header_template_field'))
|
->label(__('pages.header_template_field'))
|
||||||
->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
|
->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||||
|
->visible(fn (Get $get) => empty($get('custom_header_blade')))
|
||||||
->searchable()
|
->searchable()
|
||||||
->live()
|
->live()
|
||||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
->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;
|
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||||
|
|
||||||
|
use App\Support\PageTemplateHero;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
|
use Filament\Schemas\Components\Utilities\Get;
|
||||||
|
|
||||||
class PageSettingsSection
|
class PageSettingsSection
|
||||||
{
|
{
|
||||||
@@ -15,6 +17,13 @@ class PageSettingsSection
|
|||||||
{
|
{
|
||||||
return Section::make(__('pages.form_section_page_settings'))
|
return Section::make(__('pages.form_section_page_settings'))
|
||||||
->schema([
|
->schema([
|
||||||
|
Select::make('template')
|
||||||
|
->label(__('pages.template_field'))
|
||||||
|
->options(PageTemplateHero::templateFormOptions())
|
||||||
|
->default('default')
|
||||||
|
->live()
|
||||||
|
->columnSpanFull(),
|
||||||
|
|
||||||
FileUpload::make('featured_image')
|
FileUpload::make('featured_image')
|
||||||
->label(__('pages.featured_image_field'))
|
->label(__('pages.featured_image_field'))
|
||||||
->image()
|
->image()
|
||||||
@@ -27,7 +36,13 @@ class PageSettingsSection
|
|||||||
'4:3',
|
'4:3',
|
||||||
'1:1',
|
'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(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
Select::make('author_id')
|
Select::make('author_id')
|
||||||
@@ -62,17 +77,6 @@ class PageSettingsSection
|
|||||||
->helperText(__('pages.parent_helper_text'))
|
->helperText(__('pages.parent_helper_text'))
|
||||||
->columnSpanFull(),
|
->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')
|
TextInput::make('sort_order')
|
||||||
->label(__('pages.sort_order_field'))
|
->label(__('pages.sort_order_field'))
|
||||||
->numeric()
|
->numeric()
|
||||||
|
|||||||
@@ -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()
|
->image()
|
||||||
->disk('public')
|
->disk('public')
|
||||||
->directory('pages/sections')
|
->directory('pages/sections')
|
||||||
->imageEditor()
|
->directory('pages/sections')
|
||||||
->live()
|
->live()
|
||||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||||
->afterStateHydrated(function ($component, $state, Get $get) {
|
->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');
|
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
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class SettingForm
|
|||||||
->regex('/^[a-z0-9_-]+$/')
|
->regex('/^[a-z0-9_-]+$/')
|
||||||
->maxLength(255)
|
->maxLength(255)
|
||||||
->disabled(fn ($record) => $record !== null)
|
->disabled(fn ($record) => $record !== null)
|
||||||
|
->live(onBlur: true)
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
TextInput::make('label')
|
TextInput::make('label')
|
||||||
@@ -114,10 +115,28 @@ class SettingForm
|
|||||||
->label(__('settings.value'))
|
->label(__('settings.value'))
|
||||||
->helperText(__('settings.value_helper'))
|
->helperText(__('settings.value_helper'))
|
||||||
->required()
|
->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)
|
->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3)
|
||||||
->columnSpanFull(),
|
->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')
|
Toggle::make('value_boolean')
|
||||||
->label(__('settings.value'))
|
->label(__('settings.value'))
|
||||||
->helperText(__('settings.value_helper'))
|
->helperText(__('settings.value_helper'))
|
||||||
@@ -134,7 +153,15 @@ class SettingForm
|
|||||||
->directory('settings')
|
->directory('settings')
|
||||||
->acceptedFileTypes(['image/*', 'application/pdf', 'text/*'])
|
->acceptedFileTypes(['image/*', 'application/pdf', 'text/*'])
|
||||||
->maxSize(10240) // 10MB
|
->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')
|
DatePicker::make('value_date')
|
||||||
->label(__('settings.value'))
|
->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
|
<?php
|
||||||
|
|
||||||
use App\Models\Setting;
|
use App\Models\Setting;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
if (!function_exists('setting')) {
|
if (!function_exists('setting')) {
|
||||||
/**
|
/**
|
||||||
@@ -12,7 +14,35 @@ if (!function_exists('setting')) {
|
|||||||
*/
|
*/
|
||||||
function setting(string $key, $default = null)
|
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\Language;
|
||||||
use App\Models\Translation;
|
use App\Models\Translation;
|
||||||
|
use App\Models\SiteTranslation;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
if (!function_exists('current_language')) {
|
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')) {
|
if (!function_exists('get_language_flag')) {
|
||||||
/**
|
/**
|
||||||
* Get language flag emoji or icon
|
* Get language flag emoji or icon
|
||||||
@@ -228,7 +316,8 @@ if (!function_exists('get_language_flag')) {
|
|||||||
function get_language_flag(string $languageCode): string
|
function get_language_flag(string $languageCode): string
|
||||||
{
|
{
|
||||||
$language = Language::findByCode($languageCode);
|
$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\HeaderTemplate;
|
||||||
use App\Models\FooterTemplate;
|
use App\Models\FooterTemplate;
|
||||||
use App\Services\TemplateService;
|
use App\Services\TemplateService;
|
||||||
|
use App\Models\Page;
|
||||||
|
use App\Support\BlogStructuredData;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
@@ -40,47 +42,179 @@ class BlogController extends Controller
|
|||||||
$query = $query->whereJsonContains('tags', $request->tag);
|
$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
|
$posts = $query
|
||||||
? $query->latest('published_at')->paginate(12)
|
? $query->latest('published_at')->paginate(12)
|
||||||
: collect();
|
: 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;
|
$renderedHeader = null;
|
||||||
|
$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)
|
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||||
->latest('updated_at')
|
->latest('updated_at')
|
||||||
->first();
|
->first();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
if ($headerTemplate) {
|
if ($headerTemplate) {
|
||||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||||
|
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
|
||||||
|
$mergedData = array_merge($templateDefaults, $pageData);
|
||||||
|
|
||||||
$renderedHeader = TemplateService::replacePlaceholders(
|
$renderedHeader = TemplateService::replacePlaceholders(
|
||||||
$headerTemplate->html_content,
|
$headerTemplate->html_content,
|
||||||
$templateDefaults
|
$mergedData,
|
||||||
|
$blogPage
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render Footer Template - En son aktif olan footer template'i kullan
|
// Render Footer Template
|
||||||
$renderedFooter = null;
|
$renderedFooter = null;
|
||||||
|
$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)
|
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||||
->latest('updated_at')
|
->latest('updated_at')
|
||||||
->first();
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
if ($footerTemplate) {
|
if ($footerTemplate) {
|
||||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||||
|
$pageData = $blogPage ? ($blogPage->footer_data ?? []) : [];
|
||||||
|
$mergedData = array_merge($templateDefaults, $pageData);
|
||||||
|
|
||||||
$renderedFooter = TemplateService::replacePlaceholders(
|
$renderedFooter = TemplateService::replacePlaceholders(
|
||||||
$footerTemplate->html_content,
|
$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', [
|
return view('blog.index', [
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'posts' => $posts,
|
'posts' => $posts,
|
||||||
|
'categories' => $categories,
|
||||||
|
'carouselPosts' => $carouselPosts,
|
||||||
|
'highlightCategories' => $highlightCategories,
|
||||||
|
'popularPosts' => $popularPosts,
|
||||||
|
'sidebarCategories' => $sidebarCategories,
|
||||||
|
'tags' => $tags,
|
||||||
'renderedHeader' => $renderedHeader,
|
'renderedHeader' => $renderedHeader,
|
||||||
'renderedFooter' => $renderedFooter,
|
'renderedFooter' => $renderedFooter,
|
||||||
'meta' => [
|
'meta' => [
|
||||||
'title' => __('blog.meta-index-title'),
|
'title' => __('blog.meta-index-title'),
|
||||||
'description' => __('blog.meta-index-description'),
|
'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')
|
->orderBy('created_at', 'desc')
|
||||||
->get();
|
->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;
|
$renderedHeader = null;
|
||||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
$headerTemplate = null;
|
||||||
->latest('updated_at')
|
|
||||||
->first();
|
// 1. Öncelik: Blog sayfasında seçili header template
|
||||||
|
if ($blogPage && $blogPage->header_template_id) {
|
||||||
|
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
|
||||||
|
}
|
||||||
|
|
||||||
if ($headerTemplate) {
|
if ($headerTemplate) {
|
||||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||||
|
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
|
||||||
|
$mergedData = array_merge($templateDefaults, $pageData);
|
||||||
|
|
||||||
$renderedHeader = TemplateService::replacePlaceholders(
|
$renderedHeader = TemplateService::replacePlaceholders(
|
||||||
$headerTemplate->html_content,
|
$headerTemplate->html_content,
|
||||||
$templateDefaults,
|
$mergedData,
|
||||||
$post
|
$post
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -162,6 +305,8 @@ class BlogController extends Controller
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pageUrl = route('blog.show', $post->slug);
|
||||||
|
|
||||||
return view('blog.show', [
|
return view('blog.show', [
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'post' => $post,
|
'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),
|
'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),
|
'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,
|
'image' => $post->featured_image ? asset('storage/' . $post->featured_image) : null,
|
||||||
|
'og_type' => 'article',
|
||||||
],
|
],
|
||||||
|
'structuredData' => BlogStructuredData::forShow($post, $pageUrl),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,422 @@
|
|||||||
|
<?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,
|
||||||
|
'status' => 'waiting_document',
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 (request()->has('intern_id') && auth()->check() && auth()->user()->hasRole('super_admin')) {
|
||||||
|
$internId = request('intern_id');
|
||||||
|
} else {
|
||||||
|
if (!session()->has('intern_id')) {
|
||||||
|
return redirect()->route('intern.login')->with('error', 'Lütfen önce giriş yapın.');
|
||||||
|
}
|
||||||
|
$internId = session('intern_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$intern = CareerApplication::findOrFail($internId);
|
||||||
|
$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ı.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function verifyCertificate($code)
|
||||||
|
{
|
||||||
|
$application = CareerApplication::where('certificate_code', $code)
|
||||||
|
->where('type', 'internship')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
return view('front.career.verify', [
|
||||||
|
'application' => $application,
|
||||||
|
'meta' => [
|
||||||
|
'title' => 'Staj Bitirme Sertifikası Doğrulama - ' . $application->name,
|
||||||
|
'description' => $application->name . ' isimli stajyerimizin staj bitirme sertifikası ve performans raporu doğrulama sayfası.',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function internAdminLoginForm()
|
||||||
|
{
|
||||||
|
if (auth()->check() && auth()->user()->hasRole('super_admin')) {
|
||||||
|
return redirect()->route('intern.admin.dashboard');
|
||||||
|
}
|
||||||
|
return view('front.career.admin_login', [
|
||||||
|
'meta' => [
|
||||||
|
'title' => 'Yönetici Girişi - Staj Takip',
|
||||||
|
'description' => 'Stajyerleri yönetmek için giriş yapın.',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function internAdminLogin(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'email' => 'required|email',
|
||||||
|
'password' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (auth()->attempt($request->only('email', 'password'))) {
|
||||||
|
if (auth()->user()->hasRole('super_admin')) {
|
||||||
|
return redirect()->route('intern.admin.dashboard')->with('success', 'Yönetici girişi başarıyla gerçekleştirildi.');
|
||||||
|
}
|
||||||
|
|
||||||
|
auth()->logout();
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput($request->only('email'))
|
||||||
|
->withErrors([
|
||||||
|
'email' => 'Bu panele erişmek için super_admin yetkinizin olması gerekir.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()->back()
|
||||||
|
->withInput($request->only('email'))
|
||||||
|
->withErrors([
|
||||||
|
'email' => 'Girdiğiniz bilgiler eşleşmedi veya hatalı.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function internAdminDashboard()
|
||||||
|
{
|
||||||
|
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
|
||||||
|
return redirect()->route('intern.admin.login')->with('error', 'Lütfen önce yönetici olarak giriş yapın.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$allInterns = CareerApplication::where('type', 'internship')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$ganttInterns = CareerApplication::where('type', 'internship')
|
||||||
|
->where('status', 'accepted')
|
||||||
|
->whereNotNull('internship_start_date')
|
||||||
|
->whereNotNull('internship_end_date')
|
||||||
|
->orderBy('internship_start_date', 'asc')
|
||||||
|
->get()
|
||||||
|
->map(function ($intern) {
|
||||||
|
return [
|
||||||
|
'id' => $intern->id,
|
||||||
|
'parentId' => null,
|
||||||
|
'title' => $intern->name,
|
||||||
|
'start' => $intern->internship_start_date,
|
||||||
|
'end' => $intern->internship_end_date,
|
||||||
|
'progress' => 100
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('front.career.admin_dashboard', [
|
||||||
|
'allInterns' => $allInterns,
|
||||||
|
'ganttInterns' => $ganttInterns,
|
||||||
|
'meta' => [
|
||||||
|
'title' => 'Staj Yönetici Paneli',
|
||||||
|
'description' => 'Stajyer bilgilerini ve takvimlerini izleyin.',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function internAdminLogout()
|
||||||
|
{
|
||||||
|
auth()->logout();
|
||||||
|
return redirect()->route('intern.admin.login')->with('success', 'Yönetici oturumu sonlandırı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\HeaderTemplate;
|
||||||
use App\Models\FooterTemplate;
|
use App\Models\FooterTemplate;
|
||||||
use App\Services\TemplateService;
|
use App\Services\TemplateService;
|
||||||
|
use App\Support\PageStructuredData;
|
||||||
|
use App\Support\PageTemplateHero;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class PageController extends Controller
|
class PageController extends Controller
|
||||||
{
|
{
|
||||||
@@ -16,117 +19,32 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
// 1) is_homepage işaretli ve published (en son güncellenen)
|
$settings = $this->getSettings();
|
||||||
|
|
||||||
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||||
->where('is_homepage', true)
|
->where('is_homepage', true)
|
||||||
->where('status', 'published')
|
->where('status', 'published')
|
||||||
->latest('updated_at')
|
->latest('updated_at')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
|
// 1. Statik "home" Template Kontrolü
|
||||||
|
if (view()->exists('templates.home')) {
|
||||||
|
return $this->renderView('templates.home', $page, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hiç sayfa yoksa fallback
|
||||||
|
|
||||||
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
|
|
||||||
|
|
||||||
// Hiç sayfa yoksa: direkt home template'inin statik fallback'i ile render et
|
|
||||||
if (!$page) {
|
if (!$page) {
|
||||||
|
$meta = $this->getMeta(null, $settings);
|
||||||
|
|
||||||
return view('templates.home', [
|
return view('templates.home', [
|
||||||
'page' => null,
|
'page' => null,
|
||||||
'settings' => $settings,
|
'settings' => $settings,
|
||||||
'meta' => [
|
'meta' => $meta,
|
||||||
'title' => $settings->default_meta_title ?? config('app.name'),
|
'structuredData' => PageStructuredData::forPage(null, url('/'), $meta['title'] ?? null, $meta['description'] ?? null),
|
||||||
'description' => $settings->default_meta_description ?? null,
|
|
||||||
'image' => $settings->default_meta_image ?? null,
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$metaTitle = method_exists($page, 'translate')
|
return $this->renderPage($page, $settings);
|
||||||
? ($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,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,13 +52,127 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
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'])
|
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||||
->where('slug', $slug)
|
->where('slug', $slug)
|
||||||
->where('status', 'published')
|
->where('status', 'published')
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
|
return $this->renderPage($page, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a page with full dynamic logic
|
||||||
|
*/
|
||||||
|
protected function renderPage(Page $page, $settings)
|
||||||
|
{
|
||||||
|
$template = $page->template
|
||||||
|
?? (($page->is_homepage ?? false) ? 'home' : 'generic');
|
||||||
|
|
||||||
|
$view = view()->exists("templates.$template")
|
||||||
|
? "templates.$template"
|
||||||
|
: 'templates.generic';
|
||||||
|
|
||||||
|
return $this->renderView($view, $page, $settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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')
|
$metaTitle = method_exists($page, 'translate')
|
||||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||||
: ($page->meta_title ?? $page->title ?? null);
|
: ($page->meta_title ?? $page->title ?? null);
|
||||||
@@ -149,84 +181,80 @@ public function show($slug)
|
|||||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||||
|
|
||||||
// Use parsed_sections for easier template usage (key-value format)
|
if ($page->featured_image_url) {
|
||||||
$sections = $page->parsed_sections ?? $page->sections ?? $page->data ?? [];
|
$metaImage = $page->featured_image_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Yeni dinamik template sistemi için sections_data varsa onu da al
|
return [
|
||||||
$templatedSections = $page->templated_sections ?? collect([]);
|
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
|
||||||
|
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
|
||||||
|
'image' => $metaImage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// Template belirleme mantığı:
|
/**
|
||||||
// 1. Kullanıcı template seçmişse, onu kullan
|
* Render header template
|
||||||
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
|
*/
|
||||||
// 3. Hiçbiri yoksa, 'generic' kullan
|
protected function renderHeader(Page $page, $settings)
|
||||||
$template = $page->template
|
{
|
||||||
?? (($slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
$view = view()->exists("templates.$template")
|
// 2. Yoksa Header Template sistemini kullan
|
||||||
? "templates.$template"
|
|
||||||
: 'templates.generic';
|
|
||||||
|
|
||||||
// Render Header Template
|
|
||||||
$renderedHeader = null;
|
|
||||||
$headerTemplate = $page->headerTemplate;
|
$headerTemplate = $page->headerTemplate;
|
||||||
|
|
||||||
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
|
if (!$headerTemplate && isset($settings->default_header)) {
|
||||||
if (!$headerTemplate) {
|
$headerTemplate = HeaderTemplate::find($settings->default_header);
|
||||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
|
||||||
->latest('updated_at')
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($headerTemplate) {
|
if ($headerTemplate) {
|
||||||
// Merge template defaults with page data (page data overrides defaults)
|
|
||||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||||
$pageData = $page->header_data ?? [];
|
$pageData = $page->header_data ?? [];
|
||||||
$mergedHeaderData = array_merge($templateDefaults, $pageData);
|
$mergedHeaderData = array_merge($templateDefaults, $pageData);
|
||||||
|
|
||||||
$renderedHeader = TemplateService::replacePlaceholders(
|
return TemplateService::replacePlaceholders(
|
||||||
$headerTemplate->html_content,
|
$headerTemplate->html_content,
|
||||||
$mergedHeaderData,
|
$mergedHeaderData,
|
||||||
$page
|
$page
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render Footer Template
|
return null;
|
||||||
$renderedFooter = 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;
|
$footerTemplate = $page->footerTemplate;
|
||||||
|
|
||||||
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
|
if (!$footerTemplate && isset($settings->default_footer)) {
|
||||||
if (!$footerTemplate) {
|
$footerTemplate = FooterTemplate::find($settings->default_footer);
|
||||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
|
||||||
->latest('updated_at')
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($footerTemplate) {
|
if ($footerTemplate) {
|
||||||
// Merge template defaults with page data (page data overrides defaults)
|
|
||||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||||
$pageData = $page->footer_data ?? [];
|
$pageData = $page->footer_data ?? [];
|
||||||
$mergedFooterData = array_merge($templateDefaults, $pageData);
|
$mergedFooterData = array_merge($templateDefaults, $pageData);
|
||||||
|
|
||||||
$renderedFooter = TemplateService::replacePlaceholders(
|
return TemplateService::replacePlaceholders(
|
||||||
$footerTemplate->html_content,
|
$footerTemplate->html_content,
|
||||||
$mergedFooterData,
|
$mergedFooterData,
|
||||||
$page
|
$page
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view($view, [
|
return null;
|
||||||
'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,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,6 +276,3 @@ public function show($slug)
|
|||||||
->get();
|
->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,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class InternJournalTimeline extends Component
|
||||||
|
{
|
||||||
|
public ?Model $record = null;
|
||||||
|
|
||||||
|
public function getGithubRepoProperty(): ?string
|
||||||
|
{
|
||||||
|
return $this->record?->github_repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.intern-journal-timeline');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,60 @@
|
|||||||
|
<?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',
|
||||||
|
'certificate_code',
|
||||||
|
'transcript_markdown',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function booted()
|
||||||
|
{
|
||||||
|
static::saving(function ($model) {
|
||||||
|
if ($model->type === 'internship' && !$model->certificate_code) {
|
||||||
|
do {
|
||||||
|
$code = 'TRN-' . date('Y') . '-' . strtoupper(\Illuminate\Support\Str::random(4)) . '-' . strtoupper(\Illuminate\Support\Str::random(4));
|
||||||
|
} while (static::where('certificate_code', $code)->exists());
|
||||||
|
$model->certificate_code = $code;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-4
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\SectionTemplate;
|
use App\Models\SectionTemplate;
|
||||||
|
use App\Support\PageTemplateHero;
|
||||||
use App\Traits\HasTranslations;
|
use App\Traits\HasTranslations;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@@ -34,8 +35,10 @@ class Page extends Model
|
|||||||
// Template System
|
// Template System
|
||||||
'header_template_id',
|
'header_template_id',
|
||||||
'header_data',
|
'header_data',
|
||||||
|
'custom_header_blade',
|
||||||
'footer_template_id',
|
'footer_template_id',
|
||||||
'footer_data',
|
'footer_data',
|
||||||
|
'custom_footer_blade',
|
||||||
'sections_data',
|
'sections_data',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -78,6 +81,11 @@ class Page extends Model
|
|||||||
return $this->hasMany(Page::class, 'parent_id');
|
return $this->hasMany(Page::class, 'parent_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testimonials()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Testimonial::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the header template for the page.
|
* Get the header template for the page.
|
||||||
*/
|
*/
|
||||||
@@ -108,13 +116,14 @@ class Page extends Model
|
|||||||
return '/' . $this->slug;
|
return '/' . $this->slug;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFeaturedImageUrlAttribute()
|
public function getFeaturedImageUrlAttribute(): ?string
|
||||||
{
|
{
|
||||||
if ($this->featured_image) {
|
return PageTemplateHero::urlForPage($this);
|
||||||
return asset('storage/' . $this->featured_image);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user