Compare commits
88 Commits
main
..
truncgilcom
| Author | SHA1 | Date | |
|---|---|---|---|
| 097c6719a1 | |||
| 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.
|
||||
@@ -1,4 +1,4 @@
|
||||
<img width="1920" height="518" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" />
|
||||
<img width="50%" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" />
|
||||
|
||||
# Citrus Platform
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\BlogTopic;
|
||||
use App\Models\Blog; // Ensure this model exists and has 'status' column
|
||||
use App\Models\User;
|
||||
use App\Services\AiBlogService;
|
||||
use Illuminate\Support\Str;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class GenerateBlogAssistant extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'blog:generate-assistant {--topic= : Specific topic to generate}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate a new AI blog post draft based on active topics.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(AiBlogService $aiService)
|
||||
{
|
||||
\Illuminate\Support\Facades\Log::info('GenerateBlogAssistant triggered. Args: ' . json_encode($_SERVER['argv'] ?? []));
|
||||
$this->info('Starting AI Blog Assistant...');
|
||||
|
||||
// 1. Select Topic
|
||||
$topicName = $this->option('topic');
|
||||
$topic = null;
|
||||
|
||||
if ($topicName) {
|
||||
$topic = BlogTopic::where('name', $topicName)->first();
|
||||
if (!$topic) {
|
||||
$this->error("Topic '{$topicName}' not found.");
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
// Find oldest used active topic
|
||||
$topic = BlogTopic::active()
|
||||
->orderBy('last_used_at', 'asc') // Nulls first usually, or old dates
|
||||
->first();
|
||||
}
|
||||
|
||||
if (!$topic) {
|
||||
$this->warn('No active topics found. Please add topics to blog_topics table.');
|
||||
return 0;
|
||||
}
|
||||
|
||||
$this->info("Selected Topic: {$topic->name}");
|
||||
|
||||
// 2. Generate Content
|
||||
$this->info('Generating content with Gemini API...');
|
||||
try {
|
||||
$contentData = $aiService->generateContent($topic->name, $topic->context);
|
||||
} catch (\Exception $e) {
|
||||
$this->error('Content generation failed: ' . $e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (empty($contentData) || !isset($contentData['title']) || !isset($contentData['content'])) {
|
||||
$this->error('Valid content not returned from AI Service.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->info('Content generated successfully.');
|
||||
|
||||
// 3. Generate/Fetch Image
|
||||
$this->info('Fetching cover image...');
|
||||
$imagePath = null;
|
||||
if (isset($contentData['image_prompt'])) {
|
||||
$imagePath = $aiService->fetchAndSaveImage($topic->name, $contentData['image_title'] ?? $contentData['image_prompt']); // Pollinations uses prompt
|
||||
}
|
||||
|
||||
if (!$imagePath) {
|
||||
$this->warn('Could not fetch image, proceeding without cover image.');
|
||||
} else {
|
||||
$this->info("Image saved to: {$imagePath}");
|
||||
}
|
||||
|
||||
// 4. Create Draft Blog Post
|
||||
$author = User::first(); // Default to first user (Admin)
|
||||
|
||||
$blog = new Blog();
|
||||
$blog->title = $contentData['title'];
|
||||
$blog->slug = Str::slug($contentData['title']);
|
||||
$blog->content = $contentData['content'];
|
||||
$blog->excerpt = $contentData['excerpt'] ?? Str::limit(strip_tags($contentData['content']), 150);
|
||||
$blog->meta_title = $contentData['meta_title'] ?? $contentData['title'];
|
||||
$blog->meta_description = $contentData['meta_description'] ?? $blog->excerpt;
|
||||
$blog->status = 'draft'; // Verification required
|
||||
$blog->author_id = $author ? $author->id : 1;
|
||||
$blog->featured_image = $imagePath;
|
||||
$blog->published_at = null; // Draft
|
||||
$blog->tags = $contentData['tags'] ?? [];
|
||||
// category_id? We might need to guess or default.
|
||||
// For now leave null or set default if exists.
|
||||
|
||||
$blog->save();
|
||||
|
||||
// 5. Update Topic Usage
|
||||
$topic->last_used_at = Carbon::now();
|
||||
$topic->save();
|
||||
|
||||
$this->info("Blog post '{$blog->title}' created as DRAFT (ID: {$blog->id}).");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class ImportHtmlTemplates extends Command
|
||||
'span' => 'text.content',
|
||||
'address' => 'text.address',
|
||||
'button' => 'text.button',
|
||||
'a' => 'url.link',
|
||||
'a' => 'text.link_text',
|
||||
'li' => 'text.list_item',
|
||||
'label' => 'text.label',
|
||||
];
|
||||
@@ -260,7 +260,7 @@ class ImportHtmlTemplates extends Command
|
||||
$links = $xpath->query('.//a', $node);
|
||||
foreach ($links as $link) {
|
||||
$href = $link->getAttribute('href');
|
||||
if (empty($href) || str_starts_with($href, '#') || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue;
|
||||
if (empty($href) || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue;
|
||||
|
||||
$fixedHref = $this->fixAssetPath($href);
|
||||
if (str_ends_with($fixedHref, '.html')) {
|
||||
@@ -268,7 +268,7 @@ class ImportHtmlTemplates extends Command
|
||||
if($fixedHref == 'index') $fixedHref = '/';
|
||||
}
|
||||
|
||||
$key = $this->generateKey('url.link', $counts);
|
||||
$key = $this->generateKey('text.link_url', $counts);
|
||||
$data[$key] = $fixedHref;
|
||||
$link->setAttribute('href', "{" . $key . "}");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class BlogCategoryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = BlogCategory::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-tag';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Blog';
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class BlogResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Blog::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedDocumentText;
|
||||
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\CreateCareerApplication;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\EditCareerApplication;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Pages\ListCareerApplications;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Schemas\CareerApplicationForm;
|
||||
use App\Filament\Admin\Resources\CareerApplications\Tables\CareerApplicationsTable;
|
||||
use App\Models\CareerApplication;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class CareerApplicationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = CareerApplication::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-user-group';
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('career.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('career.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('career.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return CareerApplicationForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return CareerApplicationsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListCareerApplications::route('/'),
|
||||
'create' => CreateCareerApplication::route('/create'),
|
||||
'edit' => EditCareerApplication::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateCareerApplication extends CreateRecord
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditCareerApplication extends EditRecord
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListCareerApplications extends ListRecords
|
||||
{
|
||||
protected static string $resource = CareerApplicationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Schemas;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class CareerApplicationForm
|
||||
{
|
||||
public static function configure(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(),
|
||||
|
||||
FileUpload::make('cv_path')
|
||||
->label(__('career.cv'))
|
||||
->disk('public')
|
||||
->directory('cvs')
|
||||
->required()
|
||||
->disabled(),
|
||||
|
||||
Textarea::make('message')
|
||||
->label(__('career.message'))
|
||||
->disabled()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('status')
|
||||
->label(__('career.status'))
|
||||
->options([
|
||||
'pending' => 'Pending',
|
||||
'reviewed' => 'Reviewed',
|
||||
'rejected' => 'Rejected',
|
||||
'accepted' => 'Accepted',
|
||||
])
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\CareerApplications\Tables;
|
||||
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class CareerApplicationsTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('name')
|
||||
->label(__('career.name'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('email')
|
||||
->label(__('career.email'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('phone')
|
||||
->label(__('career.phone'))
|
||||
->searchable(),
|
||||
|
||||
TextColumn::make('type')
|
||||
->label(__('career.type'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'job' => 'success',
|
||||
'internship' => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
|
||||
|
||||
TextColumn::make('status')
|
||||
->label(__('career.status'))
|
||||
->badge()
|
||||
->color(fn (string $state): string => match ($state) {
|
||||
'pending' => 'gray',
|
||||
'reviewed' => 'info',
|
||||
'rejected' => 'danger',
|
||||
'accepted' => 'success',
|
||||
default => 'gray',
|
||||
}),
|
||||
|
||||
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' => 'Pending',
|
||||
'reviewed' => 'Reviewed',
|
||||
'rejected' => 'Rejected',
|
||||
'accepted' => 'Accepted',
|
||||
]),
|
||||
SelectFilter::make('type')
|
||||
->label(__('career.type'))
|
||||
->options([
|
||||
'job' => __('career.job'),
|
||||
'internship' => __('career.internship'),
|
||||
]),
|
||||
])
|
||||
->actions([
|
||||
Action::make('download_cv')
|
||||
->label(__('career.download_cv'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
|
||||
->openUrlInNewTab(),
|
||||
Action::make('download_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');
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ class PageResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
RelationManagers\TestimonialsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\RelationManagers;
|
||||
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class TestimonialsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'testimonials';
|
||||
|
||||
protected static ?string $title = 'Müşteri Görüşleri';
|
||||
|
||||
protected static ?string $modelLabel = 'Müşteri Görüşü';
|
||||
|
||||
protected static ?string $pluralModelLabel = 'Müşteri Görüşleri';
|
||||
|
||||
protected static ?string $recordTitleAttribute = 'name';
|
||||
|
||||
public static function canViewForRecord(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): bool
|
||||
{
|
||||
// Sadece Müşteri Görüşleri sayfası veya testimonials şablonu kullananlar için göster
|
||||
return $ownerRecord->slug === 'musteri-gorusleri' ||
|
||||
$ownerRecord->template === 'corporate.testimonials' ||
|
||||
$ownerRecord->is_homepage;
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
TextInput::make('name')
|
||||
->label('Müşteri Adı')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('position')
|
||||
->label('Şirket / Ünvan')
|
||||
->maxLength(255),
|
||||
Textarea::make('content')
|
||||
->label('Görüş / Yorum')
|
||||
->required()
|
||||
->columnSpanFull(),
|
||||
Select::make('rating')
|
||||
->label('Puan')
|
||||
->options([
|
||||
1 => '1 Yıldız',
|
||||
2 => '2 Yıldız',
|
||||
3 => '3 Yıldız',
|
||||
4 => '4 Yıldız',
|
||||
5 => '5 Yıldız',
|
||||
])
|
||||
->default(5)
|
||||
->required(),
|
||||
FileUpload::make('avatar')
|
||||
->label('Profil Resmi')
|
||||
->image()
|
||||
->directory('testimonials'),
|
||||
Toggle::make('is_active')
|
||||
->label('Aktif')
|
||||
->default(true),
|
||||
TextInput::make('sort_order')
|
||||
->label('Sıralama')
|
||||
->numeric()
|
||||
->default(0),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('name')
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('name')
|
||||
->label('Ad')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('position')
|
||||
->label('Ünvan')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('rating')
|
||||
->label('Puan')
|
||||
->sortable(),
|
||||
Tables\Columns\IconColumn::make('is_active')
|
||||
->label('Aktif')
|
||||
->boolean()
|
||||
->sortable(),
|
||||
Tables\Columns\TextColumn::make('sort_order')
|
||||
->label('Sıra')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make(),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,9 @@ use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionBuilderSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionTemplatesSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\TranslationsSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\LogosSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\PartnersSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\BankAccountsSection;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
@@ -49,6 +52,11 @@ class PageForm
|
||||
SectionTemplatesSection::make(),
|
||||
FooterTemplateSection::make(),
|
||||
|
||||
// Full Width - Logos (Only for Logolarımız template)
|
||||
LogosSection::make(),
|
||||
PartnersSection::make(),
|
||||
BankAccountsSection::make(),
|
||||
|
||||
// Full Width - Translations
|
||||
TranslationsSection::make(),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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')
|
||||
->required(),
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class FooterTemplateSection
|
||||
{
|
||||
public static function make(): Section
|
||||
@@ -17,9 +19,35 @@ class FooterTemplateSection
|
||||
return Section::make(__('pages.form_section_footer_template'))
|
||||
->description(__('pages.form_section_footer_template_desc'))
|
||||
->schema([
|
||||
Select::make('custom_footer_blade')
|
||||
->label('Custom Footer Blade')
|
||||
->options(function () {
|
||||
$files = File::glob(resource_path('views/partials/*.blade.php'));
|
||||
$options = [];
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["partials.{$name}"] = "Partial: {$name}";
|
||||
}
|
||||
|
||||
// Add other directories if needed
|
||||
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
|
||||
foreach ($componentFiles as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["components.front.{$name}"] = "Component: {$name}";
|
||||
}
|
||||
|
||||
return $options;
|
||||
})
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Select a physical Blade file to use as footer instead of a database template.')
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('footer_template_id')
|
||||
->label(__('pages.footer_template_field'))
|
||||
->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => empty($get('custom_footer_blade')))
|
||||
->searchable()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
|
||||
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class HeaderTemplateSection
|
||||
{
|
||||
public static function make(): Section
|
||||
@@ -17,9 +19,35 @@ class HeaderTemplateSection
|
||||
return Section::make(__('pages.form_section_header_template'))
|
||||
->description(__('pages.form_section_header_template_desc'))
|
||||
->schema([
|
||||
Select::make('custom_header_blade')
|
||||
->label('Custom Header Blade')
|
||||
->options(function () {
|
||||
$files = File::glob(resource_path('views/partials/*.blade.php'));
|
||||
$options = [];
|
||||
foreach ($files as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["partials.{$name}"] = "Partial: {$name}";
|
||||
}
|
||||
|
||||
// Add other directories if needed
|
||||
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
|
||||
foreach ($componentFiles as $file) {
|
||||
$name = basename($file, '.blade.php');
|
||||
$options["components.front.{$name}"] = "Component: {$name}";
|
||||
}
|
||||
|
||||
return $options;
|
||||
})
|
||||
->searchable()
|
||||
->preload()
|
||||
->helperText('Select a physical Blade file to use as header instead of a database template.')
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('header_template_id')
|
||||
->label(__('pages.header_template_field'))
|
||||
->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => empty($get('custom_header_blade')))
|
||||
->searchable()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class LogosSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('Logolar'))
|
||||
->description(__('Bu sayfada gösterilecek logoları buradan yönetebilirsiniz.'))
|
||||
->schema([
|
||||
Grid::make(3)
|
||||
->schema([
|
||||
FileUpload::make('data.logo_1')
|
||||
->label(__('Logo 1'))
|
||||
->disk('public'),
|
||||
|
||||
FileUpload::make('data.logo_2')
|
||||
->label(__('Logo 2'))
|
||||
->disk('public'),
|
||||
|
||||
FileUpload::make('data.logo_3')
|
||||
->label(__('Logo 3'))
|
||||
->disk('public'),
|
||||
]),
|
||||
])
|
||||
->visible(fn (Get $get): bool => $get('template') === 'corporate.logos')
|
||||
->columnSpanFull()
|
||||
->collapsible();
|
||||
}
|
||||
}
|
||||
@@ -69,13 +69,19 @@ class PageSettingsSection
|
||||
'landing' => __('pages.template_landing'),
|
||||
'blog' => __('pages.template_blog'),
|
||||
'contact' => __('pages.template_contact'),
|
||||
'home' => 'Home',
|
||||
'corporate.testimonials' => 'Müşteri Görüşleri (Kurumsal)',
|
||||
'corporate.logos' => 'Logolarımız (Kurumsal)',
|
||||
'corporate.partners' => 'Çözüm Ortaklarımız (Kurumsal)',
|
||||
'corporate.bank-accounts' => 'Banka Bilgilerimiz (Kurumsal)',
|
||||
'corporate.online-payment' => 'Online Ödeme (Kurumsal)',
|
||||
])
|
||||
->default('default')
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
->label(__('pages.sort_order_field'))
|
||||
->numeric()
|
||||
TextInput::make('sort_order')
|
||||
->label(__('pages.sort_order_field'))
|
||||
->numeric()
|
||||
->default(0)
|
||||
->helperText(__('pages.sort_order_helper_text'))
|
||||
->columnSpanFull(),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?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([
|
||||
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(),
|
||||
])
|
||||
->grid(4)
|
||||
->columnSpanFull()
|
||||
->reorderableWithButtons()
|
||||
->collapsible()
|
||||
->cloneable(),
|
||||
])
|
||||
->visible(fn (Get $get): bool => $get('template') === 'corporate.partners')
|
||||
->columnSpanFull()
|
||||
->collapsible();
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ class SectionBuilderSection
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('pages/sections')
|
||||
->imageEditor()
|
||||
->directory('pages/sections')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(function ($component, $state, Get $get) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateProductCategory extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProductCategoryResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditProductCategory extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProductCategoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProductCategories extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProductCategoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories;
|
||||
|
||||
use App\Filament\Admin\Resources\ProductCategories\Pages\CreateProductCategory;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Pages\EditProductCategory;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Pages\ListProductCategories;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Schemas\ProductCategoryForm;
|
||||
use App\Filament\Admin\Resources\ProductCategories\Tables\ProductCategoriesTable;
|
||||
use App\Models\ProductCategory;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProductCategoryResource extends Resource
|
||||
{
|
||||
protected static ?string $model = ProductCategory::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('product_categories.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('product_categories.menu_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('product_categories.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('product_categories.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ProductCategoryForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ProductCategoriesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListProductCategories::route('/'),
|
||||
'create' => CreateProductCategory::route('/create'),
|
||||
'edit' => EditProductCategory::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Models\ProductCategory;
|
||||
|
||||
class ProductCategoryForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->schema([
|
||||
Section::make(__('product_categories.general_section'))
|
||||
->schema([
|
||||
TextInput::make('slug')
|
||||
->label(__('product_categories.slug'))
|
||||
->required()
|
||||
->unique(ignoreRecord: true),
|
||||
Select::make('parent_id')
|
||||
->label(__('product_categories.parent_category'))
|
||||
->options(function () {
|
||||
return ProductCategory::all()->mapWithKeys(function ($category) {
|
||||
$title = is_array($category->title)
|
||||
? ($category->title[app()->getLocale()] ?? \Illuminate\Support\Arr::first($category->title))
|
||||
: $category->title;
|
||||
return [$category->id => $title ?? ('Category #' . $category->id)];
|
||||
});
|
||||
})
|
||||
->searchable(),
|
||||
TextInput::make('sort_order')
|
||||
->label(__('product_categories.sort_order'))
|
||||
->numeric()
|
||||
->default(0),
|
||||
Toggle::make('is_active')
|
||||
->label(__('product_categories.is_active'))
|
||||
->default(true),
|
||||
])->columns(2),
|
||||
|
||||
Section::make(__('product_categories.translations_section'))
|
||||
->schema([
|
||||
TranslationTabs::make([
|
||||
'title' => [
|
||||
'type' => 'text',
|
||||
'label' => __('product_categories.title'),
|
||||
'required' => true,
|
||||
],
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\ProductCategories\Tables;
|
||||
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
|
||||
class ProductCategoriesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('title')
|
||||
->label(__('product_categories.title'))
|
||||
->getStateUsing(fn ($record) => $record->translate('title'))
|
||||
->searchable(query: function ($query, $search) {
|
||||
return $query->whereHas('translations', function ($q) use ($search) {
|
||||
$q->where('field_name', 'title')
|
||||
->where('field_value', 'like', "%{$search}%")
|
||||
->where('language_code', app()->getLocale());
|
||||
});
|
||||
}),
|
||||
TextColumn::make('slug')
|
||||
->label(__('product_categories.slug'))
|
||||
->searchable(),
|
||||
TextColumn::make('parent.title')
|
||||
->getStateUsing(fn ($record) => $record->parent?->translate('title'))
|
||||
->label(__('product_categories.parent')),
|
||||
TextColumn::make('sort_order')
|
||||
->label(__('product_categories.sort_order'))
|
||||
->sortable(),
|
||||
ToggleColumn::make('is_active')
|
||||
->label(__('product_categories.is_active')),
|
||||
TextColumn::make('created_at')
|
||||
->label(__('product_categories.created_at'))
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\Products\ProductResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateProduct extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ProductResource::class;
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use App\Filament\Admin\Resources\Products\ProductResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditProduct extends EditRecord
|
||||
{
|
||||
protected static string $resource = ProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$data = $this->form->getState();
|
||||
TranslationTabs::saveTranslations($this->record, $data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Products\ProductResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListProducts extends ListRecords
|
||||
{
|
||||
protected static string $resource = ProductResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Products;
|
||||
|
||||
use App\Filament\Admin\Resources\Products\Pages\CreateProduct;
|
||||
use App\Filament\Admin\Resources\Products\Pages\EditProduct;
|
||||
use App\Filament\Admin\Resources\Products\Pages\ListProducts;
|
||||
use App\Filament\Admin\Resources\Products\Schemas\ProductForm;
|
||||
use App\Filament\Admin\Resources\Products\Tables\ProductsTable;
|
||||
use App\Models\Product;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class ProductResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Product::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCube;
|
||||
|
||||
public static function getNavigationGroup(): ?string
|
||||
{
|
||||
return __('products.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('products.menu_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('products.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('products.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return ProductForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return ProductsTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => ListProducts::route('/'),
|
||||
'create' => CreateProduct::route('/create'),
|
||||
'edit' => EditProduct::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?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(false),
|
||||
])
|
||||
->visible(fn (Get $get) => $get('type') === 'product')
|
||||
->columnSpanFull();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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'))
|
||||
->required()
|
||||
->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'))
|
||||
->required()
|
||||
->unique(ignoreRecord: true),
|
||||
Select::make('type')
|
||||
->label(__('products.type'))
|
||||
->options([
|
||||
'product' => __('products.product'),
|
||||
'service' => __('products.service'),
|
||||
])
|
||||
->required(),
|
||||
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'))
|
||||
->required()
|
||||
->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' => true,
|
||||
],
|
||||
'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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,25 @@ class EditSetting extends EditRecord
|
||||
return __('settings.edit');
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
$record = $this->getRecord();
|
||||
|
||||
if (isset($data['type'])) {
|
||||
$type = $data['type'];
|
||||
$virtualField = 'value_' . $type;
|
||||
|
||||
try {
|
||||
// Model accessor'ını kullanarak değeri al
|
||||
$data[$virtualField] = $record->{$virtualField};
|
||||
} catch (\Exception $e) {
|
||||
// Accessor bulunamazsa devam et
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -42,6 +42,7 @@ class SettingForm
|
||||
->regex('/^[a-z0-9_-]+$/')
|
||||
->maxLength(255)
|
||||
->disabled(fn ($record) => $record !== null)
|
||||
->live(onBlur: true)
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('label')
|
||||
@@ -114,10 +115,28 @@ class SettingForm
|
||||
->label(__('settings.value'))
|
||||
->helperText(__('settings.value_helper'))
|
||||
->required()
|
||||
->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json']))
|
||||
->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json']) && !in_array($get('key'), ['default_header', 'default_footer']))
|
||||
->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3)
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('value_header_template')
|
||||
->label('Default Header Template')
|
||||
->options(\App\Models\HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => $get('key') === 'default_header')
|
||||
->statePath('value')
|
||||
->searchable()
|
||||
->preload()
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('value_footer_template')
|
||||
->label('Default Footer Template')
|
||||
->options(\App\Models\FooterTemplate::where('is_active', true)->pluck('title', 'id'))
|
||||
->visible(fn (Get $get) => $get('key') === 'default_footer')
|
||||
->statePath('value')
|
||||
->searchable()
|
||||
->preload()
|
||||
->columnSpanFull(),
|
||||
|
||||
Toggle::make('value_boolean')
|
||||
->label(__('settings.value'))
|
||||
->helperText(__('settings.value_helper'))
|
||||
@@ -134,7 +153,15 @@ class SettingForm
|
||||
->directory('settings')
|
||||
->acceptedFileTypes(['image/*', 'application/pdf', 'text/*'])
|
||||
->maxSize(10240) // 10MB
|
||||
->columnSpanFull(),
|
||||
->openable()
|
||||
->downloadable()
|
||||
->previewable(true)
|
||||
->columnSpanFull()
|
||||
->afterStateHydrated(function ($component, $state, $record) {
|
||||
if ($record && $record->type === 'file') {
|
||||
$component->state($record->value);
|
||||
}
|
||||
}),
|
||||
|
||||
DatePicker::make('value_date')
|
||||
->label(__('settings.value'))
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateSiteTranslation extends CreateRecord
|
||||
{
|
||||
protected static string $resource = SiteTranslationResource::class;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditSiteTranslation extends EditRecord
|
||||
{
|
||||
protected static string $resource = SiteTranslationResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
class ListSiteTranslations extends ListRecords
|
||||
{
|
||||
protected static string $resource = SiteTranslationResource::class;
|
||||
|
||||
public bool $useDataGrid = false;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
parent::mount();
|
||||
|
||||
// Session'dan görünüm tercihini al, yoksa varsayılan olarak false (eski görünüm)
|
||||
$this->useDataGrid = session('site_translations_use_datagrid', false);
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
$useDataGrid = session('site_translations_use_datagrid', false);
|
||||
|
||||
return [
|
||||
Action::make('toggleView')
|
||||
->label($useDataGrid ? 'Tablo Görünümü' : 'DataGrid Görünümü')
|
||||
->icon($useDataGrid ? 'heroicon-o-table-cells' : 'heroicon-o-squares-2x2')
|
||||
->color('gray')
|
||||
->action(function () {
|
||||
$currentValue = session('site_translations_use_datagrid', false);
|
||||
$newValue = !$currentValue;
|
||||
session(['site_translations_use_datagrid' => $newValue]);
|
||||
session()->save(); // Session'ı hemen kaydet
|
||||
|
||||
// Sayfayı yenile
|
||||
$this->redirect($this->getResource()::getUrl('index'));
|
||||
}),
|
||||
CreateAction::make()
|
||||
->visible(!$useDataGrid), // Sadece eski görünümde göster
|
||||
];
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$useDataGrid = session('site_translations_use_datagrid', false);
|
||||
|
||||
if ($useDataGrid) {
|
||||
return view('filament.admin.resources.site-translations.datagrid')
|
||||
->layout($this->getLayout());
|
||||
}
|
||||
|
||||
return parent::render();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Schemas;
|
||||
|
||||
use App\Models\Language;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Grid;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class SiteTranslationForm
|
||||
{
|
||||
public static function make(Schema $schema): Schema
|
||||
{
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
|
||||
$translationFields = [];
|
||||
foreach ($languages as $language) {
|
||||
$translationFields[] = Textarea::make($language->code)
|
||||
->label($language->name . " ({$language->code})")
|
||||
->rows(2);
|
||||
}
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
Section::make()
|
||||
->schema([
|
||||
Textarea::make('key')
|
||||
->label('Key / Original Text')
|
||||
->required()
|
||||
->columnSpanFull()
|
||||
->helperText('The original text to be translated. Use this exact text in t() helper.'),
|
||||
|
||||
Group::make()
|
||||
->schema($translationFields)
|
||||
->statePath('translations')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations;
|
||||
|
||||
use App\Filament\Admin\Resources\SiteTranslations\Pages;
|
||||
use App\Filament\Admin\Resources\SiteTranslations\Schemas\SiteTranslationForm;
|
||||
use App\Filament\Admin\Resources\SiteTranslations\Tables\SiteTranslationsTable;
|
||||
use App\Models\SiteTranslation;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
use UnitEnum;
|
||||
use BackedEnum;
|
||||
|
||||
class SiteTranslationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = SiteTranslation::class;
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-language';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Settings';
|
||||
|
||||
protected static ?int $navigationSort = 100;
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return SiteTranslationForm::make($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return SiteTranslationsTable::make($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListSiteTranslations::route('/'),
|
||||
'create' => Pages\CreateSiteTranslation::route('/create'),
|
||||
'edit' => Pages\EditSiteTranslation::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('Site Translations');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('Translation');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('Translations');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SiteTranslations\Tables;
|
||||
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
use App\Models\Language;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Actions\ImportAction;
|
||||
use Filament\Actions\ExportAction;
|
||||
use App\Filament\Imports\SiteTranslationImporter;
|
||||
use App\Filament\Exports\SiteTranslationExporter;
|
||||
|
||||
class SiteTranslationsTable
|
||||
{
|
||||
public static function make(Table $table): Table
|
||||
{
|
||||
$defaultLocale = app()->getLocale();
|
||||
|
||||
return $table
|
||||
->headerActions([
|
||||
ImportAction::make()
|
||||
->importer(SiteTranslationImporter::class),
|
||||
ExportAction::make()
|
||||
->exporter(SiteTranslationExporter::class)
|
||||
])
|
||||
->columns([
|
||||
TextColumn::make('key')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->limit(50)
|
||||
->wrap(),
|
||||
|
||||
TextColumn::make("translations.{$defaultLocale}")
|
||||
->label("Translation ({$defaultLocale})")
|
||||
->searchable()
|
||||
->limit(50)
|
||||
->wrap()
|
||||
->placeholder('No translation'),
|
||||
|
||||
TextColumn::make('created_at')
|
||||
->dateTime()
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->recordActions([
|
||||
EditAction::make()
|
||||
->modalWidth('xl')
|
||||
->form(function () {
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
|
||||
$translationFields = [];
|
||||
foreach ($languages as $language) {
|
||||
$translationFields[] = Textarea::make($language->code)
|
||||
->label($language->name . " ({$language->code})")
|
||||
->rows(2);
|
||||
}
|
||||
|
||||
return [
|
||||
Section::make()
|
||||
->schema([
|
||||
Textarea::make('key')
|
||||
->label('Key / Original Text')
|
||||
->required()
|
||||
->columnSpanFull()
|
||||
->helperText('The original text to be translated. Use this exact text in t() helper.'),
|
||||
|
||||
Group::make()
|
||||
->schema($translationFields)
|
||||
->statePath('translations')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
];
|
||||
}),
|
||||
DeleteAction::make(),
|
||||
])
|
||||
->toolbarActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Exports;
|
||||
|
||||
use App\Models\SiteTranslation;
|
||||
use App\Models\Language;
|
||||
use Filament\Actions\Exports\ExportColumn;
|
||||
use Filament\Actions\Exports\Exporter;
|
||||
use Filament\Actions\Exports\Models\Export;
|
||||
|
||||
class SiteTranslationExporter extends Exporter
|
||||
{
|
||||
protected static ?string $model = SiteTranslation::class;
|
||||
|
||||
public static function getColumns(): array
|
||||
{
|
||||
$columns = [
|
||||
ExportColumn::make('key')->label('Key'),
|
||||
];
|
||||
|
||||
// Veritabanı bağlantısı yoksa veya tablo yoksa hata vermemesi için try-catch veya kontrol
|
||||
try {
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
foreach ($languages as $language) {
|
||||
$columns[] = ExportColumn::make("translations.{$language->code}")
|
||||
->label($language->name . " ({$language->code})");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Migration henüz çalışmadıysa
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public static function getCompletedNotificationBody(Export $export): string
|
||||
{
|
||||
$body = 'Your site translation export has completed and ' . number_format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.';
|
||||
|
||||
if ($failedRowsCount = $export->getFailedRowsCount()) {
|
||||
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Imports;
|
||||
|
||||
use App\Models\SiteTranslation;
|
||||
use App\Models\Language;
|
||||
use Filament\Actions\Imports\ImportColumn;
|
||||
use Filament\Actions\Imports\Importer;
|
||||
use Filament\Actions\Imports\Models\Import;
|
||||
|
||||
class SiteTranslationImporter extends Importer
|
||||
{
|
||||
protected static ?string $model = SiteTranslation::class;
|
||||
|
||||
public static function getColumns(): array
|
||||
{
|
||||
$columns = [
|
||||
ImportColumn::make('key')
|
||||
->label('Key')
|
||||
->requiredMapping()
|
||||
->rules(['required']),
|
||||
];
|
||||
|
||||
try {
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
foreach ($languages as $language) {
|
||||
$columns[] = ImportColumn::make($language->code)
|
||||
->label($language->name . " ({$language->code})");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
//
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
public function resolveRecord(): ?SiteTranslation
|
||||
{
|
||||
if (empty($this->data['key'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Key üzerinden kaydı bul veya yeni oluştur
|
||||
// Not: key alanı text olduğu için tam eşleşme arıyoruz
|
||||
return SiteTranslation::firstOrNew([
|
||||
'key' => $this->data['key'],
|
||||
]);
|
||||
}
|
||||
|
||||
protected function beforeSave(): void
|
||||
{
|
||||
$translations = $this->record->translations ?? [];
|
||||
|
||||
$languages = Language::where('is_active', true)->pluck('code')->toArray();
|
||||
|
||||
foreach ($languages as $code) {
|
||||
if (array_key_exists($code, $this->data)) {
|
||||
$value = $this->data[$code];
|
||||
|
||||
// Boş string de olsa güncelleyelim, null ise (CSV'de yoksa) dokunmayalım
|
||||
if ($value !== null) {
|
||||
$translations[$code] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->record->translations = $translations;
|
||||
|
||||
// Hash'i manuel oluşturmaya gerek yok, model event'i halledecek ama
|
||||
// new record ise ve event çalışmazsa diye garantiye alalım (import toplu insert yapabilir mi? Hayır record record işler)
|
||||
if (!$this->record->exists && empty($this->record->hash)) {
|
||||
$this->record->hash = md5($this->record->key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getCompletedNotificationBody(Import $import): string
|
||||
{
|
||||
$body = 'Your site translation import has completed and ' . number_format($import->successful_rows) . ' ' . str('row')->plural($import->successful_rows) . ' imported.';
|
||||
|
||||
if ($failedRowsCount = $import->getFailedRowsCount()) {
|
||||
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to import.';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
if (!function_exists('setting')) {
|
||||
/**
|
||||
@@ -12,7 +14,35 @@ if (!function_exists('setting')) {
|
||||
*/
|
||||
function setting(string $key, $default = null)
|
||||
{
|
||||
return Setting::get($key, $default);
|
||||
// Cache settings to avoid database queries on every call
|
||||
// Cache will store the full setting object to access type info
|
||||
$setting = Cache::remember("app_setting_{$key}", 60 * 60 * 24, function () use ($key) {
|
||||
return Setting::where('key', $key)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
});
|
||||
|
||||
if (!$setting) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$value = $setting->value;
|
||||
|
||||
// Handle file/image types
|
||||
if (in_array($setting->type, ['file', 'image']) || $key === 'home_hero') {
|
||||
if (empty($value)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
// If value is already a URL, return it
|
||||
if (filter_var($value, FILTER_VALIDATE_URL)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Return storage URL
|
||||
return Storage::url($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use App\Models\Language;
|
||||
use App\Models\Translation;
|
||||
use App\Models\SiteTranslation;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
if (!function_exists('current_language')) {
|
||||
@@ -279,3 +280,19 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,7 @@ use App\Models\BlogComment;
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Models\Page;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
@@ -44,37 +45,80 @@ class BlogController extends Controller
|
||||
? $query->latest('published_at')->paginate(12)
|
||||
: collect();
|
||||
|
||||
// Render Header Template - En son aktif olan header template'i kullan
|
||||
// Blog sayfa ayarlarını bul (varsa)
|
||||
$blogPage = Page::where('slug', 'blog')->first();
|
||||
|
||||
// Render Header Template
|
||||
$renderedHeader = null;
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
$headerTemplate = null;
|
||||
|
||||
// 1. Öncelik: Blog sayfasında seçili header template
|
||||
if ($blogPage && $blogPage->header_template_id) {
|
||||
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
|
||||
}
|
||||
|
||||
// 2. Öncelik: Varsayılan aktif template (KALDIRILDI - Default static header kullanılmalı)
|
||||
/*
|
||||
if (!$headerTemplate) {
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
*/
|
||||
|
||||
if ($headerTemplate) {
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
|
||||
$mergedData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$templateDefaults
|
||||
$mergedData,
|
||||
$blogPage
|
||||
);
|
||||
}
|
||||
|
||||
// Render Footer Template - En son aktif olan footer template'i kullan
|
||||
// Render Footer Template
|
||||
$renderedFooter = null;
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
$footerTemplate = null;
|
||||
|
||||
// 1. Öncelik: Blog sayfasında seçili footer template
|
||||
if ($blogPage && $blogPage->footer_template_id) {
|
||||
$footerTemplate = FooterTemplate::find($blogPage->footer_template_id);
|
||||
}
|
||||
|
||||
// 2. Öncelik: Varsayılan aktif template
|
||||
if (!$footerTemplate) {
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($footerTemplate) {
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$pageData = $blogPage ? ($blogPage->footer_data ?? []) : [];
|
||||
$mergedData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$templateDefaults
|
||||
$mergedData,
|
||||
$blogPage
|
||||
);
|
||||
}
|
||||
|
||||
// Kategorileri al (filtreleme için)
|
||||
$categories = class_exists(\App\Models\BlogCategory::class) ? \App\Models\BlogCategory::where('is_active', true)->orderBy('sort_order')->get() : collect();
|
||||
|
||||
if ($request->ajax()) {
|
||||
return view('blog.partials.posts', [
|
||||
'posts' => $posts
|
||||
]);
|
||||
}
|
||||
|
||||
return view('blog.index', [
|
||||
'settings' => $settings,
|
||||
'posts' => $posts,
|
||||
'categories' => $categories,
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => [
|
||||
@@ -132,17 +176,26 @@ class BlogController extends Controller
|
||||
->orderBy('created_at', 'desc')
|
||||
->get();
|
||||
|
||||
// Render Header Template - En son aktif olan header template'i kullan
|
||||
// Blog sayfa ayarlarını bul (varsa)
|
||||
$blogPage = Page::where('slug', 'blog')->first();
|
||||
|
||||
// Render Header Template
|
||||
$renderedHeader = null;
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
$headerTemplate = null;
|
||||
|
||||
// 1. Öncelik: Blog sayfasında seçili header template
|
||||
if ($blogPage && $blogPage->header_template_id) {
|
||||
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
|
||||
}
|
||||
|
||||
if ($headerTemplate) {
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
|
||||
$mergedData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$templateDefaults,
|
||||
$mergedData,
|
||||
$post
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<?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['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;
|
||||
|
||||
CareerApplication::create([
|
||||
'name' => $request->name,
|
||||
'email' => $request->email,
|
||||
'phone' => $request->phone,
|
||||
'type' => $request->type ?? 'job',
|
||||
'cv_path' => $cvPath,
|
||||
'nda_path' => $ndaPath,
|
||||
'contract_path' => $contractPath,
|
||||
'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'));
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
@@ -16,217 +17,242 @@ class PageController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// 1) is_homepage işaretli ve published (en son güncellenen)
|
||||
$settings = $this->getSettings();
|
||||
|
||||
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||
->where('is_homepage', true)
|
||||
->where('status', 'published')
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
|
||||
|
||||
|
||||
// 1. Statik "home" Template Kontrolü
|
||||
if (view()->exists('templates.home')) {
|
||||
return $this->renderView('templates.home', $page, $settings);
|
||||
}
|
||||
|
||||
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
|
||||
|
||||
// Hiç sayfa yoksa: direkt home template'inin statik fallback'i ile render et
|
||||
// Hiç sayfa yoksa fallback
|
||||
if (!$page) {
|
||||
return view('templates.home', [
|
||||
'page' => null,
|
||||
'settings' => $settings,
|
||||
'meta' => [
|
||||
'title' => $settings->default_meta_title ?? config('app.name'),
|
||||
'description' => $settings->default_meta_description ?? null,
|
||||
'image' => $settings->default_meta_image ?? null,
|
||||
],
|
||||
'meta' => $this->getMeta(null, $settings),
|
||||
]);
|
||||
}
|
||||
|
||||
$metaTitle = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||
: ($page->meta_title ?? $page->title ?? null);
|
||||
|
||||
$metaDescription = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
// Section verisini al - Hem eski hem yeni sistemi destekle
|
||||
$sections = $page->sections ?? $page->data ?? [];
|
||||
|
||||
// Yeni dinamik template sistemi için sections_data varsa onu da al
|
||||
$templatedSections = $page->templated_sections ?? collect([]);
|
||||
|
||||
// Template belirleme mantığı:
|
||||
// 1. Kullanıcı template seçmişse, onu kullan
|
||||
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
|
||||
// 3. Hiçbiri yoksa, 'generic' kullan
|
||||
$template = $page->template
|
||||
?? (($page->slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
|
||||
|
||||
$view = view()->exists("templates.$template")
|
||||
? "templates.$template"
|
||||
: 'templates.generic';
|
||||
|
||||
// Render Header Template
|
||||
$renderedHeader = null;
|
||||
$headerTemplate = $page->headerTemplate;
|
||||
|
||||
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
|
||||
if (!$headerTemplate) {
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($headerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $page->header_data ?? [];
|
||||
$mergedHeaderData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$mergedHeaderData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
// Render Footer Template
|
||||
$renderedFooter = null;
|
||||
$footerTemplate = $page->footerTemplate;
|
||||
|
||||
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
|
||||
if (!$footerTemplate) {
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($footerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$pageData = $page->footer_data ?? [];
|
||||
$mergedFooterData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$mergedFooterData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'settings' => $settings,
|
||||
'sections' => $sections,
|
||||
'templatedSections' => $templatedSections,
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => [
|
||||
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
|
||||
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
|
||||
'image' => $settings->default_meta_image ?? null,
|
||||
],
|
||||
]);
|
||||
return $this->renderPage($page, $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a specific page by slug
|
||||
*/
|
||||
public function show($slug)
|
||||
public function show($slug)
|
||||
{
|
||||
return $this->handleShow($slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle nested page routes (e.g. /kurumsal/hakkimizda)
|
||||
*/
|
||||
public function showNested($parentSlug, $slug)
|
||||
{
|
||||
return $this->handleShow($slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle akademi.truncgil.com.tr subdomain
|
||||
*/
|
||||
public function showAkademi()
|
||||
{
|
||||
return $this->handleShow('truncgil-akademi');
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified show logic
|
||||
*/
|
||||
protected function handleShow($slug)
|
||||
{
|
||||
$settings = $this->getSettings();
|
||||
|
||||
// 3a. Akademi Sayfası Yönlendirmeleri
|
||||
if ($slug === 'truncgil-akademi') {
|
||||
$host = request()->getHost();
|
||||
$path = request()->path();
|
||||
|
||||
// Eğer ana domainden geliyorsa -> Subdomain'e yönlendir
|
||||
if ($host !== 'akademi.truncgil.com.tr') {
|
||||
return redirect()->to('https://akademi.truncgil.com.tr/', 301);
|
||||
}
|
||||
|
||||
// Eğer subdoman'de ama slug ile geliyorsa (/truncgil-akademi) -> Ana dizine yönlendir (/)
|
||||
if ($host === 'akademi.truncgil.com.tr' && $path !== '/') {
|
||||
return redirect()->to('https://akademi.truncgil.com.tr/', 301);
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Statik View Kontrolü
|
||||
if (view()->exists("templates.$slug")) {
|
||||
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||
->where('slug', $slug)
|
||||
->where('status', 'published')
|
||||
->first();
|
||||
|
||||
return $this->renderView("templates.$slug", $page, $settings);
|
||||
}
|
||||
|
||||
// 2. Normal Dinamik Akış (DB Kaydı Zorunlu)
|
||||
$page = Page::with(['headerTemplate', 'footerTemplate'])
|
||||
->where('slug', $slug)
|
||||
->where('status', 'published')
|
||||
->firstOrFail();
|
||||
|
||||
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
|
||||
return $this->renderPage($page, $settings);
|
||||
}
|
||||
|
||||
$metaTitle = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||
: ($page->meta_title ?? $page->title ?? null);
|
||||
|
||||
$metaDescription = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
// Use parsed_sections for easier template usage (key-value format)
|
||||
$sections = $page->parsed_sections ?? $page->sections ?? $page->data ?? [];
|
||||
|
||||
// Yeni dinamik template sistemi için sections_data varsa onu da al
|
||||
$templatedSections = $page->templated_sections ?? collect([]);
|
||||
|
||||
// Template belirleme mantığı:
|
||||
// 1. Kullanıcı template seçmişse, onu kullan
|
||||
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
|
||||
// 3. Hiçbiri yoksa, 'generic' kullan
|
||||
/**
|
||||
* Render a page with full dynamic logic
|
||||
*/
|
||||
protected function renderPage(Page $page, $settings)
|
||||
{
|
||||
$template = $page->template
|
||||
?? (($slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
|
||||
?? (($page->is_homepage ?? false) ? 'home' : 'generic');
|
||||
|
||||
$view = view()->exists("templates.$template")
|
||||
? "templates.$template"
|
||||
: 'templates.generic';
|
||||
|
||||
return $this->renderView($view, $page, $settings);
|
||||
}
|
||||
|
||||
// Render Header Template
|
||||
/**
|
||||
* Shared rendering logic for both static and dynamic templates
|
||||
*/
|
||||
protected function renderView(string $view, ?Page $page, $settings)
|
||||
{
|
||||
$renderedHeader = null;
|
||||
$renderedFooter = null;
|
||||
|
||||
if ($page) {
|
||||
$renderedHeader = $this->renderHeader($page, $settings);
|
||||
$renderedFooter = $this->renderFooter($page, $settings);
|
||||
}
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'settings' => $settings,
|
||||
'sections' => $page ? ($page->parsed_sections ?? $page->sections ?? $page->data ?? []) : [],
|
||||
'templatedSections' => $page ? ($page->templated_sections ?? collect([])) : collect([]),
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => $this->getMeta($page, $settings),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings as an object
|
||||
*/
|
||||
protected function getSettings()
|
||||
{
|
||||
if (!class_exists(Setting::class)) {
|
||||
return (object) [];
|
||||
}
|
||||
|
||||
return (object) Setting::where('is_active', true)
|
||||
->pluck('value', 'key')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meta information for a page
|
||||
*/
|
||||
protected function getMeta(?Page $page, $settings)
|
||||
{
|
||||
$metaTitle = null;
|
||||
$metaDescription = null;
|
||||
$metaImage = $settings->default_meta_image ?? null;
|
||||
|
||||
if ($page) {
|
||||
$metaTitle = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_title') ?: $page->translate('title'))
|
||||
: ($page->meta_title ?? $page->title ?? null);
|
||||
|
||||
$metaDescription = method_exists($page, 'translate')
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
if ($page->featured_image) {
|
||||
$metaImage = asset('storage/' . $page->featured_image);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
|
||||
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
|
||||
'image' => $metaImage,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render header template
|
||||
*/
|
||||
protected function renderHeader(Page $page, $settings)
|
||||
{
|
||||
// 1. Önce özel blade dosyası var mı kontrol et
|
||||
if (!empty($page->custom_header_blade) && view()->exists($page->custom_header_blade)) {
|
||||
return view($page->custom_header_blade, ['page' => $page])->render();
|
||||
}
|
||||
|
||||
// 2. Yoksa Header Template sistemini kullan
|
||||
$headerTemplate = $page->headerTemplate;
|
||||
|
||||
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
|
||||
if (!$headerTemplate) {
|
||||
$headerTemplate = HeaderTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
if (!$headerTemplate && isset($settings->default_header)) {
|
||||
$headerTemplate = HeaderTemplate::find($settings->default_header);
|
||||
}
|
||||
|
||||
if ($headerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $headerTemplate->default_data ?? [];
|
||||
$pageData = $page->header_data ?? [];
|
||||
$mergedHeaderData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedHeader = TemplateService::replacePlaceholders(
|
||||
return TemplateService::replacePlaceholders(
|
||||
$headerTemplate->html_content,
|
||||
$mergedHeaderData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
// Render Footer Template
|
||||
$renderedFooter = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render footer template
|
||||
*/
|
||||
protected function renderFooter(Page $page, $settings)
|
||||
{
|
||||
// 1. Önce özel blade dosyası var mı kontrol et
|
||||
if (!empty($page->custom_footer_blade) && view()->exists($page->custom_footer_blade)) {
|
||||
return view($page->custom_footer_blade, ['page' => $page])->render();
|
||||
}
|
||||
|
||||
// 2. Yoksa Footer Template sistemini kullan
|
||||
$footerTemplate = $page->footerTemplate;
|
||||
|
||||
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
|
||||
if (!$footerTemplate) {
|
||||
$footerTemplate = FooterTemplate::where('is_active', true)
|
||||
->latest('updated_at')
|
||||
->first();
|
||||
if (!$footerTemplate && isset($settings->default_footer)) {
|
||||
$footerTemplate = FooterTemplate::find($settings->default_footer);
|
||||
}
|
||||
|
||||
if ($footerTemplate) {
|
||||
// Merge template defaults with page data (page data overrides defaults)
|
||||
$templateDefaults = $footerTemplate->default_data ?? [];
|
||||
$pageData = $page->footer_data ?? [];
|
||||
$mergedFooterData = array_merge($templateDefaults, $pageData);
|
||||
|
||||
$renderedFooter = TemplateService::replacePlaceholders(
|
||||
return TemplateService::replacePlaceholders(
|
||||
$footerTemplate->html_content,
|
||||
$mergedFooterData,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'settings' => $settings,
|
||||
'sections' => $sections,
|
||||
'templatedSections' => $templatedSections,
|
||||
'renderedHeader' => $renderedHeader,
|
||||
'renderedFooter' => $renderedFooter,
|
||||
'meta' => [
|
||||
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
|
||||
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
|
||||
'image' => $settings->default_meta_image ?? null,
|
||||
],
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,6 +274,3 @@ public function show($slug)
|
||||
->get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function process(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'name' => 'required|string|max:255',
|
||||
'surname' => 'required|string|max:255',
|
||||
'email' => 'required|email|max:255',
|
||||
'phone' => 'required|string|max:20',
|
||||
'price' => 'required|numeric|min:1',
|
||||
'address' => 'nullable|string',
|
||||
'agreement' => 'accepted',
|
||||
], [
|
||||
'name.required' => 'Lütfen adınızı giriniz.',
|
||||
'surname.required' => 'Lütfen soyadınızı giriniz.',
|
||||
'email.required' => 'Lütfen e-posta adresinizi giriniz.',
|
||||
'email.email' => 'Geçerli bir e-posta adresi giriniz.',
|
||||
'phone.required' => 'Lütfen telefon numaranızı giriniz.',
|
||||
'price.required' => 'Lütfen ödeme tutarını giriniz.',
|
||||
'price.min' => 'Ödeme tutarı en az 1 TL olmalıdır.',
|
||||
'address.required' => 'Lütfen ödeme açıklamasını giriniz.',
|
||||
'agreement.accepted' => 'Lütfen sözleşmeyi onaylayınız.',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
Log::info('Payment Validation Failed', $validator->errors()->toArray());
|
||||
|
||||
return back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$merchant_id = '127117';
|
||||
$merchant_key = 's9GH5FMuxFtqjxyJ';
|
||||
$merchant_salt = 'qTHTx3j4FjieXPP3';
|
||||
|
||||
$price = $request->input('price') * 100; // Krş cinsinden
|
||||
|
||||
// Get user IP (handling Cloudflare or standard)
|
||||
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} else {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
$user_ip = $ip;
|
||||
|
||||
$merchant_oid = time(); // Benzersiz sipariş no
|
||||
$email = $request->input('email');
|
||||
$payment_amount = $price;
|
||||
$currency = 'TL'; // Default
|
||||
$test_mode = 0; // Canlı mod
|
||||
|
||||
$user_name = $request->input('name').' '.$request->input('surname');
|
||||
$user_address = $request->input('address') ?: 'Adres Belirtilmemiş'; // Fallback if not provided
|
||||
$user_phone = $request->input('phone');
|
||||
|
||||
// Callbacks should ideally be routes we define, but user provided specific URLs.
|
||||
// We will use routes relative to current domain or the ones user specified if they are absolute?
|
||||
// The user script had: "https://www.truncgil.com.tr/pay.php?ok"
|
||||
// I should probably use the current app's URL.
|
||||
$merchant_ok_url = route('payment.success');
|
||||
$merchant_fail_url = route('payment.fail');
|
||||
|
||||
$user_basket = base64_encode(json_encode([
|
||||
['Web / Mobil Yazılım', $payment_amount, 1],
|
||||
]));
|
||||
|
||||
$no_installment = 0;
|
||||
$max_installment = 9;
|
||||
$debug_on = 1;
|
||||
$timeout_limit = 30;
|
||||
|
||||
$hash_str = $merchant_id.$user_ip.$merchant_oid.$email.$payment_amount.$user_basket.$no_installment.$max_installment.$currency.$test_mode;
|
||||
$paytr_token = base64_encode(hash_hmac('sha256', $hash_str.$merchant_salt, $merchant_key, true));
|
||||
|
||||
$post_vals = [
|
||||
'merchant_id' => $merchant_id,
|
||||
'user_ip' => $user_ip,
|
||||
'merchant_oid' => $merchant_oid,
|
||||
'email' => $email,
|
||||
'lang' => 'tr',
|
||||
'payment_amount' => $payment_amount,
|
||||
'paytr_token' => $paytr_token,
|
||||
'user_basket' => $user_basket,
|
||||
'debug_on' => $debug_on,
|
||||
'no_installment' => $no_installment,
|
||||
'max_installment' => $max_installment,
|
||||
'user_name' => $user_name,
|
||||
'user_address' => $user_address,
|
||||
'user_phone' => $user_phone,
|
||||
'merchant_ok_url' => $merchant_ok_url,
|
||||
'merchant_fail_url' => $merchant_fail_url,
|
||||
'timeout_limit' => $timeout_limit,
|
||||
'currency' => $currency,
|
||||
'test_mode' => $test_mode,
|
||||
];
|
||||
|
||||
// Log request for debugging
|
||||
Log::info('PayTR Request', [
|
||||
'merchant_id' => $merchant_id,
|
||||
'merchant_oid' => $merchant_oid,
|
||||
'email' => $email,
|
||||
'payment_amount' => $payment_amount,
|
||||
'user_ip' => $user_ip,
|
||||
]);
|
||||
|
||||
// Curl request
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, 'https://www.paytr.com/odeme/api/get-token');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_vals);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
$result = curl_exec($ch);
|
||||
|
||||
$curlError = curl_error($ch);
|
||||
$curlErrno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlErrno) {
|
||||
Log::error('PayTR cURL error', ['errno' => $curlErrno, 'error' => $curlError]);
|
||||
return back()->with('error', 'PAYTR bağlantı hatası. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
if ($result === false || empty($result)) {
|
||||
Log::error('PayTR returned empty response');
|
||||
return back()->with('error', 'Ödeme sisteminden yanıt alınamadı. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
// Log the raw response for debugging
|
||||
Log::info('PayTR Raw Response', ['response' => $result]);
|
||||
|
||||
$result = json_decode($result, 1);
|
||||
|
||||
// Check if result is valid
|
||||
if (!$result || !is_array($result)) {
|
||||
Log::error('PayTR API returned invalid response', ['response' => $result]);
|
||||
return back()->with('error', 'Ödeme sisteminden geçersiz yanıt alındı. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
if (!isset($result['status'])) {
|
||||
Log::error('PayTR API response missing status', ['response' => $result]);
|
||||
return back()->with('error', 'Ödeme sisteminden beklenmeyen yanıt alındı. Lütfen tekrar deneyin.');
|
||||
}
|
||||
|
||||
if ($result['status'] == 'success') {
|
||||
$token = $result['token'];
|
||||
|
||||
// Re-render the online-payment page but with the token
|
||||
// We need to fetch the page content again
|
||||
// Assuming the slug is 'online-odeme' or similar, or we can just pass a dummy page object
|
||||
// if we are lazy, but correct way is to fetch the page associated with the route.
|
||||
// Since we are POSTing here, we don't know the Page model context unless we fetch it.
|
||||
// Let's try to assume 'online-odeme' which is common.
|
||||
|
||||
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
|
||||
if (! $page) {
|
||||
$page = new Page(['title' => 'Online Ödeme', 'content' => '', 'excerpt' => 'Please complete your payment below.']);
|
||||
}
|
||||
|
||||
return view('templates.corporate.online-payment', [
|
||||
'page' => $page,
|
||||
'paytr_token' => $token,
|
||||
]);
|
||||
|
||||
} else {
|
||||
return back()->with('error', 'PAYTR failed. reason:'.$result['reason']);
|
||||
}
|
||||
}
|
||||
|
||||
public function success(Request $request)
|
||||
{
|
||||
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
|
||||
if ($page) {
|
||||
return redirect()->route('page.show', $page->slug)->with('success', 'Your payment has been charged successfully. This will appear on your credit card statement as "PAYTR ODEME HIZMETLERI"');
|
||||
}
|
||||
|
||||
return redirect()->route('home')->with('success', 'Your payment has been charged successfully.');
|
||||
}
|
||||
|
||||
public function fail(Request $request)
|
||||
{
|
||||
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
|
||||
if ($page) {
|
||||
return redirect()->route('page.show', $page->slug)->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
|
||||
}
|
||||
|
||||
return redirect()->route('home')->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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 (Hero only) for all products
|
||||
return view('front.products.landing', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
|
||||
}
|
||||
}
|
||||
@@ -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,53 @@
|
||||
<?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=()');
|
||||
|
||||
// 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;";
|
||||
$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,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,25 @@
|
||||
<?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',
|
||||
'git_knowledge',
|
||||
'ai_usage',
|
||||
'message',
|
||||
'status',
|
||||
];
|
||||
}
|
||||
@@ -34,8 +34,10 @@ class Page extends Model
|
||||
// Template System
|
||||
'header_template_id',
|
||||
'header_data',
|
||||
'custom_header_blade',
|
||||
'footer_template_id',
|
||||
'footer_data',
|
||||
'custom_footer_blade',
|
||||
'sections_data',
|
||||
];
|
||||
|
||||
@@ -78,6 +80,11 @@ class Page extends Model
|
||||
return $this->hasMany(Page::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function testimonials()
|
||||
{
|
||||
return $this->hasMany(Testimonial::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the header template for the page.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\HasTranslations;
|
||||
|
||||
class Product extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'type',
|
||||
'product_category_id',
|
||||
'hero_image',
|
||||
'content',
|
||||
'view_template',
|
||||
'external_url',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
'landing_page_data',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
'content',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'title' => 'array',
|
||||
'content' => 'array',
|
||||
'landing_page_data' => 'array',
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::saving(function ($model) {
|
||||
// Eğer title boşsa ve translations içinden bir title geliyorsa onu ata
|
||||
if (empty($model->title)) {
|
||||
$translations = request()->input('translations');
|
||||
$defaultLocale = app()->getLocale();
|
||||
|
||||
if (is_array($translations)) {
|
||||
if (!empty($translations[$defaultLocale]['title'])) {
|
||||
$model->title = $translations[$defaultLocale]['title'];
|
||||
} else {
|
||||
foreach ($translations as $locale => $fields) {
|
||||
if (!empty($fields['title'])) {
|
||||
$model->title = $fields['title'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($model->title)) {
|
||||
$model->title = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class, 'product_category_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Traits\HasTranslations;
|
||||
|
||||
class ProductCategory extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes, HasTranslations;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'slug',
|
||||
'parent_id',
|
||||
'sort_order',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $translatable = [
|
||||
'title',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'title' => 'array',
|
||||
];
|
||||
|
||||
public static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::saving(function ($model) {
|
||||
// Eğer title boşsa ve translations içinden bir title geliyorsa onu ata
|
||||
if (empty($model->title)) {
|
||||
$translations = request()->input('translations');
|
||||
$defaultLocale = app()->getLocale();
|
||||
|
||||
if (is_array($translations)) {
|
||||
// Önce varsayılan dildeki çeviriye bak
|
||||
if (!empty($translations[$defaultLocale]['title'])) {
|
||||
$model->title = $translations[$defaultLocale]['title'];
|
||||
}
|
||||
// Yoksa ilk bulduğu dolu çeviriyi al
|
||||
else {
|
||||
foreach ($translations as $locale => $fields) {
|
||||
if (!empty($fields['title'])) {
|
||||
$model->title = $fields['title'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hala boşsa boş bir array veya string ata (veritabanı hatasını önlemek için)
|
||||
if (empty($model->title)) {
|
||||
$model->title = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
return $this->belongsTo(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children()
|
||||
{
|
||||
return $this->hasMany(ProductCategory::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function products()
|
||||
{
|
||||
return $this->hasMany(Product::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SiteTranslation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'hash',
|
||||
'key',
|
||||
'translations',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'translations' => 'array',
|
||||
];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function ($model) {
|
||||
if (empty($model->hash)) {
|
||||
$model->hash = md5($model->key);
|
||||
}
|
||||
});
|
||||
|
||||
static::updating(function ($model) {
|
||||
// Key değiştiyse hash'i güncelle
|
||||
if ($model->isDirty('key')) {
|
||||
$model->hash = md5($model->key);
|
||||
}
|
||||
});
|
||||
|
||||
static::saved(function ($model) {
|
||||
Cache::forget("site_translation_{$model->hash}");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation for a specific key
|
||||
*/
|
||||
public static function getTranslation(string $key): string
|
||||
{
|
||||
$hash = md5($key);
|
||||
$locale = app()->getLocale();
|
||||
|
||||
// Cache'ten veya DB'den al
|
||||
// Cache süresi: 24 saat
|
||||
$translation = Cache::remember("site_translation_{$hash}", 60 * 60 * 24, function () use ($hash, $key) {
|
||||
return static::firstOrCreate(
|
||||
['hash' => $hash],
|
||||
['key' => $key]
|
||||
);
|
||||
});
|
||||
|
||||
// Eğer kayıt yeni oluşturulduysa ve key eksikse (concurrency nedeniyle), key'i set et
|
||||
if ($translation->wasRecentlyCreated && empty($translation->translations)) {
|
||||
// Yeni oluşturuldu, henüz çeviri yok.
|
||||
// Varsayılan olarak key'i döndür.
|
||||
return $key;
|
||||
}
|
||||
|
||||
$translations = $translation->translations ?? [];
|
||||
|
||||
if (isset($translations[$locale]) && !empty($translations[$locale])) {
|
||||
return $translations[$locale];
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Testimonial extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'page_id',
|
||||
'name',
|
||||
'position',
|
||||
'content',
|
||||
'rating',
|
||||
'avatar',
|
||||
'is_active',
|
||||
'sort_order',
|
||||
];
|
||||
|
||||
public function page()
|
||||
{
|
||||
return $this->belongsTo(Page::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\BlogCategory;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class BlogCategoryPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:BlogCategory');
|
||||
}
|
||||
|
||||
public function view(AuthUser $authUser, BlogCategory $blogCategory): bool
|
||||
{
|
||||
return $authUser->can('View:BlogCategory');
|
||||
}
|
||||
|
||||
public function create(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Create:BlogCategory');
|
||||
}
|
||||
|
||||
public function update(AuthUser $authUser, BlogCategory $blogCategory): bool
|
||||
{
|
||||
return $authUser->can('Update:BlogCategory');
|
||||
}
|
||||
|
||||
public function delete(AuthUser $authUser, BlogCategory $blogCategory): bool
|
||||
{
|
||||
return $authUser->can('Delete:BlogCategory');
|
||||
}
|
||||
|
||||
public function restore(AuthUser $authUser, BlogCategory $blogCategory): bool
|
||||
{
|
||||
return $authUser->can('Restore:BlogCategory');
|
||||
}
|
||||
|
||||
public function forceDelete(AuthUser $authUser, BlogCategory $blogCategory): bool
|
||||
{
|
||||
return $authUser->can('ForceDelete:BlogCategory');
|
||||
}
|
||||
|
||||
public function forceDeleteAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ForceDeleteAny:BlogCategory');
|
||||
}
|
||||
|
||||
public function restoreAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('RestoreAny:BlogCategory');
|
||||
}
|
||||
|
||||
public function replicate(AuthUser $authUser, BlogCategory $blogCategory): bool
|
||||
{
|
||||
return $authUser->can('Replicate:BlogCategory');
|
||||
}
|
||||
|
||||
public function reorder(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Reorder:BlogCategory');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\ProductCategory;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ProductCategoryPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:ProductCategory');
|
||||
}
|
||||
|
||||
public function view(AuthUser $authUser, ProductCategory $productCategory): bool
|
||||
{
|
||||
return $authUser->can('View:ProductCategory');
|
||||
}
|
||||
|
||||
public function create(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Create:ProductCategory');
|
||||
}
|
||||
|
||||
public function update(AuthUser $authUser, ProductCategory $productCategory): bool
|
||||
{
|
||||
return $authUser->can('Update:ProductCategory');
|
||||
}
|
||||
|
||||
public function delete(AuthUser $authUser, ProductCategory $productCategory): bool
|
||||
{
|
||||
return $authUser->can('Delete:ProductCategory');
|
||||
}
|
||||
|
||||
public function restore(AuthUser $authUser, ProductCategory $productCategory): bool
|
||||
{
|
||||
return $authUser->can('Restore:ProductCategory');
|
||||
}
|
||||
|
||||
public function forceDelete(AuthUser $authUser, ProductCategory $productCategory): bool
|
||||
{
|
||||
return $authUser->can('ForceDelete:ProductCategory');
|
||||
}
|
||||
|
||||
public function forceDeleteAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ForceDeleteAny:ProductCategory');
|
||||
}
|
||||
|
||||
public function restoreAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('RestoreAny:ProductCategory');
|
||||
}
|
||||
|
||||
public function replicate(AuthUser $authUser, ProductCategory $productCategory): bool
|
||||
{
|
||||
return $authUser->can('Replicate:ProductCategory');
|
||||
}
|
||||
|
||||
public function reorder(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Reorder:ProductCategory');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ProductPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:Product');
|
||||
}
|
||||
|
||||
public function view(AuthUser $authUser, Product $product): bool
|
||||
{
|
||||
return $authUser->can('View:Product');
|
||||
}
|
||||
|
||||
public function create(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Create:Product');
|
||||
}
|
||||
|
||||
public function update(AuthUser $authUser, Product $product): bool
|
||||
{
|
||||
return $authUser->can('Update:Product');
|
||||
}
|
||||
|
||||
public function delete(AuthUser $authUser, Product $product): bool
|
||||
{
|
||||
return $authUser->can('Delete:Product');
|
||||
}
|
||||
|
||||
public function restore(AuthUser $authUser, Product $product): bool
|
||||
{
|
||||
return $authUser->can('Restore:Product');
|
||||
}
|
||||
|
||||
public function forceDelete(AuthUser $authUser, Product $product): bool
|
||||
{
|
||||
return $authUser->can('ForceDelete:Product');
|
||||
}
|
||||
|
||||
public function forceDeleteAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ForceDeleteAny:Product');
|
||||
}
|
||||
|
||||
public function restoreAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('RestoreAny:Product');
|
||||
}
|
||||
|
||||
public function replicate(AuthUser $authUser, Product $product): bool
|
||||
{
|
||||
return $authUser->can('Replicate:Product');
|
||||
}
|
||||
|
||||
public function reorder(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Reorder:Product');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as AuthUser;
|
||||
use App\Models\SiteTranslation;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class SiteTranslationPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ViewAny:SiteTranslation');
|
||||
}
|
||||
|
||||
public function view(AuthUser $authUser, SiteTranslation $siteTranslation): bool
|
||||
{
|
||||
return $authUser->can('View:SiteTranslation');
|
||||
}
|
||||
|
||||
public function create(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Create:SiteTranslation');
|
||||
}
|
||||
|
||||
public function update(AuthUser $authUser, SiteTranslation $siteTranslation): bool
|
||||
{
|
||||
return $authUser->can('Update:SiteTranslation');
|
||||
}
|
||||
|
||||
public function delete(AuthUser $authUser, SiteTranslation $siteTranslation): bool
|
||||
{
|
||||
return $authUser->can('Delete:SiteTranslation');
|
||||
}
|
||||
|
||||
public function restore(AuthUser $authUser, SiteTranslation $siteTranslation): bool
|
||||
{
|
||||
return $authUser->can('Restore:SiteTranslation');
|
||||
}
|
||||
|
||||
public function forceDelete(AuthUser $authUser, SiteTranslation $siteTranslation): bool
|
||||
{
|
||||
return $authUser->can('ForceDelete:SiteTranslation');
|
||||
}
|
||||
|
||||
public function forceDeleteAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('ForceDeleteAny:SiteTranslation');
|
||||
}
|
||||
|
||||
public function restoreAny(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('RestoreAny:SiteTranslation');
|
||||
}
|
||||
|
||||
public function replicate(AuthUser $authUser, SiteTranslation $siteTranslation): bool
|
||||
{
|
||||
return $authUser->can('Replicate:SiteTranslation');
|
||||
}
|
||||
|
||||
public function reorder(AuthUser $authUser): bool
|
||||
{
|
||||
return $authUser->can('Reorder:SiteTranslation');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,7 +21,14 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
setlocale(LC_ALL, 'tr_TR.UTF-8', 'tr_TR', 'turkish');
|
||||
|
||||
// Page model için observer kaydet
|
||||
// Page model için observer kaydet
|
||||
Page::observe(PageObserver::class);
|
||||
|
||||
if ($this->app->environment('production') || $this->app->environment('staging')) {
|
||||
\Illuminate\Support\Facades\URL::forceScheme('https');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,24 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Routing\Middleware\SubstituteBindings;
|
||||
use Illuminate\Session\Middleware\StartSession;
|
||||
use Illuminate\View\Middleware\ShareErrorsFromSession;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use App\Models\Language;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Http\Middleware\SetLocale;
|
||||
|
||||
class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
public function panel(Panel $panel): Panel
|
||||
{
|
||||
$languages = [];
|
||||
try {
|
||||
if (Schema::hasTable('languages')) {
|
||||
$languages = Language::where('is_active', true)->get();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Migration çalışmamış olabilir
|
||||
}
|
||||
|
||||
return $panel
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
@@ -55,11 +68,23 @@ class AdminPanelProvider extends PanelProvider
|
||||
SubstituteBindings::class,
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
SetLocale::class,
|
||||
])
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
])
|
||||
->login()
|
||||
->profile() // Profil sayfası ve şifre değiştirme aktif
|
||||
->userMenuItems([
|
||||
// Dilleri menüye ekle
|
||||
...collect($languages)->map(function ($language) {
|
||||
return MenuItem::make()
|
||||
->label($language->native_name)
|
||||
->url(url('/lang/' . $language->code))
|
||||
->icon('heroicon-m-language')
|
||||
->sort(100); // En sonda görünsün
|
||||
})->toArray()
|
||||
])
|
||||
->assets([
|
||||
\Filament\Support\Assets\Css::make('citrus', resource_path('css/citrus.css')),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class AiBlogService
|
||||
{
|
||||
protected $geminiKey;
|
||||
protected $unsplashKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->geminiKey = config('services.gemini.key', env('GEMINI_API_KEY'));
|
||||
$this->unsplashKey = config('services.unsplash.access_key', env('UNSPLASH_ACCESS_KEY'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate blog content using Gemini API
|
||||
*/
|
||||
public function generateContent(string $topic, ?string $context = null): array
|
||||
{
|
||||
$prompt = $this->buildPrompt($topic, $context);
|
||||
|
||||
// Debug logging
|
||||
$keyStatus = empty($this->geminiKey) ? 'MISSING' : 'PRESENT (' . substr($this->geminiKey, 0, 5) . '...)';
|
||||
Log::info("Gemini Service: Key status: $keyStatus");
|
||||
|
||||
try {
|
||||
$response = Http::withoutVerifying()
|
||||
->retry(3, 10000, function ($exception, $request) {
|
||||
if ($exception instanceof \Illuminate\Http\Client\RequestException && $exception->response->status() === 429) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
->withHeaders(['Content-Type' => 'application/json'])
|
||||
->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$this->geminiKey}", [
|
||||
'contents' => [
|
||||
[
|
||||
'parts' => [
|
||||
['text' => $prompt]
|
||||
]
|
||||
]
|
||||
],
|
||||
'generationConfig' => [
|
||||
'temperature' => 0.7,
|
||||
'topK' => 40,
|
||||
'topP' => 0.95,
|
||||
'maxOutputTokens' => 8192,
|
||||
'responseMimeType' => 'application/json',
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('Gemini API Error Status: ' . $response->status());
|
||||
Log::error('Gemini API Error Body: ' . $response->body());
|
||||
throw new \Exception('Gemini API request failed: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) {
|
||||
Log::error('Gemini API Invalid Response: ' . json_encode($data));
|
||||
throw new \Exception('Invalid response from Gemini API.');
|
||||
}
|
||||
|
||||
$jsonContent = $data['candidates'][0]['content']['parts'][0]['text'];
|
||||
|
||||
// Clean markdown json blocks if present
|
||||
$jsonContent = str_replace(['```json', '```'], '', $jsonContent);
|
||||
|
||||
return json_decode($jsonContent, true);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('AI Blog Generation Exception: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the prompt for Gemini
|
||||
*/
|
||||
private function buildPrompt(string $topic, ?string $context = null): string
|
||||
{
|
||||
$contextStr = $context ? "Additional Context: {$context}" : "";
|
||||
|
||||
return <<<EOT
|
||||
You are an expert content writer for "Trunçgil Teknoloji", a leading technology company specializing in web software, mobile apps, IoT, and digital transformation.
|
||||
Write a comprehensive, SEO-friendly blog post about the topic: "{$topic}".
|
||||
{$contextStr}
|
||||
|
||||
**IMPORTANT**: All content in the JSON response (title, content, excerpt, meta_data) MUST BE IN TURKISH language. Only the `image_prompt` should be in English.
|
||||
|
||||
Requirements:
|
||||
1. **Format**: Return ONLY a valid JSON object. No markdown formatting outside the JSON structure.
|
||||
2. **Language**: Turkish (Türkçe).
|
||||
3. **Tone**: Professional, authoritative, yet accessible. Enthusiastic about technology.
|
||||
4. **Brand Integration**: Naturally mention "Trunçgil Teknoloji" at least 2-3 times. Position Trunçgil Teknoloji as a solution provider or expert in this field. Use phrases like "Trunçgil Teknoloji olarak biz...", "Deneyimli ekibimizle...", etc.
|
||||
5. **Structure**:
|
||||
* `title`: Catchy, SEO-optimized title (Max 60 chars) - IN TURKISH.
|
||||
* `meta_title`: SEO Meta Title - IN TURKISH.
|
||||
* `meta_description`: SEO Meta Description (Max 160 chars) - IN TURKISH.
|
||||
* `content`: Full HTML blog post content.
|
||||
* Use <h2> and <h3> tags for headings.
|
||||
* Use <p> for paragraphs.
|
||||
* Include a bulleted list (<ul>/<li>) if appropriate.
|
||||
* Do NOT use <h1> or <html>/<body> tags.
|
||||
* Ensure the content is informative and valuable (min 600 words).
|
||||
* ALL CONTENT MUST BE TURKISH.
|
||||
* `excerpt`: A short summary (1-2 sentences) - IN TURKISH.
|
||||
* `tags`: Array of 5-8 relevant tags (Turkish).
|
||||
* `image_prompt`: A creative English prompt to generate a cover image for this article using an AI image generator.
|
||||
|
||||
JSON Schema:
|
||||
{
|
||||
"title": "string",
|
||||
"meta_title": "string",
|
||||
"meta_description": "string",
|
||||
"content": "html_string",
|
||||
"excerpt": "string",
|
||||
"tags": ["string"],
|
||||
"image_prompt": "string"
|
||||
}
|
||||
EOT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and save a cover image
|
||||
*/
|
||||
public function fetchAndSaveImage(string $keyword, string $imagePrompt): ?string
|
||||
{
|
||||
$imageUrl = null;
|
||||
|
||||
// 1. Try Unsplash if key exists
|
||||
if ($this->unsplashKey) {
|
||||
try {
|
||||
$response = Http::withoutVerifying()->get('https://api.unsplash.com/search/photos', [
|
||||
'query' => $keyword,
|
||||
'orientation' => 'landscape',
|
||||
'per_page' => 1,
|
||||
'client_id' => $this->unsplashKey
|
||||
]);
|
||||
|
||||
if ($response->successful() && !empty($response['results'])) {
|
||||
$imageUrl = $response['results'][0]['urls']['regular'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('Unsplash API failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to Pollinations AI (using the generated prompt)
|
||||
if (!$imageUrl) {
|
||||
// Encode the prompt
|
||||
$encodedPrompt = urlencode($imagePrompt . " high quality, technological, photorealistic, 4k");
|
||||
$imageUrl = "https://image.pollinations.ai/prompt/{$encodedPrompt}";
|
||||
}
|
||||
|
||||
if (!$imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Download and Save
|
||||
try {
|
||||
$imageContent = Http::withoutVerifying()->get($imageUrl)->body();
|
||||
$filename = 'blog/ai-generated-' . Str::slug($keyword) . '-' . time() . '.jpg';
|
||||
|
||||
Storage::disk('public')->put($filename, $imageContent);
|
||||
|
||||
return $filename;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Image download failed: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@ class TemplateService
|
||||
|
||||
'url' => TextInput::make("{$dataKey}.{$placeholder}")
|
||||
->label($label)
|
||||
->url()
|
||||
// ->url() // Allow #, mailto, tel, relative paths
|
||||
->maxLength(500),
|
||||
|
||||
'tel' => TextInput::make("{$dataKey}.{$placeholder}")
|
||||
@@ -173,8 +173,6 @@ class TemplateService
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('templates/images')
|
||||
->imageEditor()
|
||||
->imageEditorAspectRatios([null, '16:9', '4:3', '1:1'])
|
||||
->maxSize(5120),
|
||||
|
||||
'images' => FileUpload::make("{$dataKey}.{$placeholder}")
|
||||
@@ -183,7 +181,6 @@ class TemplateService
|
||||
->disk('public')
|
||||
->directory('templates/images')
|
||||
->multiple()
|
||||
->imageEditor()
|
||||
->maxSize(5120)
|
||||
->maxFiles(10),
|
||||
|
||||
|
||||
@@ -42,22 +42,51 @@ trait HasTranslations
|
||||
{
|
||||
$languageCode = $languageCode ?? $this->getCurrentLanguageCode();
|
||||
|
||||
// 1. Try translations table
|
||||
$translation = $this->getTranslation($fieldName, $languageCode);
|
||||
|
||||
if ($translation) {
|
||||
if ($translation && !empty($translation->field_value)) {
|
||||
return $translation->field_value;
|
||||
}
|
||||
|
||||
// Fallback to default language
|
||||
// 2. Fallback to default language from translations table
|
||||
if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) {
|
||||
$defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode());
|
||||
if ($defaultTranslation) {
|
||||
if ($defaultTranslation && !empty($defaultTranslation->field_value)) {
|
||||
return $defaultTranslation->field_value;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to original field value
|
||||
return $this->{$fieldName} ?? null;
|
||||
// 3. Fallback to original field value
|
||||
$originalValue = $this->{$fieldName} ?? null;
|
||||
|
||||
// Handle JSON string if not casted
|
||||
if (is_string($originalValue) && (str_starts_with($originalValue, '{') || str_starts_with($originalValue, '['))) {
|
||||
$decoded = json_decode($originalValue, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$originalValue = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
// If the original value is an array (e.g. JSON cast), try to find translation inside it
|
||||
if (is_array($originalValue)) {
|
||||
if (isset($originalValue[$languageCode]) && !empty($originalValue[$languageCode])) {
|
||||
return $originalValue[$languageCode];
|
||||
}
|
||||
if ($fallback && isset($originalValue[$this->getDefaultLanguageCode()]) && !empty($originalValue[$this->getDefaultLanguageCode()])) {
|
||||
return $originalValue[$this->getDefaultLanguageCode()];
|
||||
}
|
||||
|
||||
// Return first non-empty value if available
|
||||
foreach ($originalValue as $val) {
|
||||
if (!empty($val)) {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
return $originalValue;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->web(append: [
|
||||
\App\Http\Middleware\SetLocale::class,
|
||||
\App\Http\Middleware\EnsureSecurityHeaders::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
|
||||
Generated
+676
-715
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Class Namespace
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value sets the root class namespace for Livewire component classes in
|
||||
| your application. This value will change where component auto-discovery
|
||||
| finds components. It's also referenced by the file creation commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'class_namespace' => 'App\\Livewire',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| View Path
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value is used to specify where Livewire component Blade templates are
|
||||
| stored when running file creation commands like `artisan make:livewire`.
|
||||
| It is also used if you choose to omit a component's render() method.
|
||||
|
|
||||
*/
|
||||
|
||||
'view_path' => resource_path('views/livewire'),
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Layout
|
||||
|---------------------------------------------------------------------------
|
||||
| The view that will be used as the layout when rendering a single component
|
||||
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
|
||||
| In this case, the view returned by CreatePost will render into $slot.
|
||||
|
|
||||
*/
|
||||
|
||||
'layout' => 'components.layouts.app',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Lazy Loading Placeholder
|
||||
|---------------------------------------------------------------------------
|
||||
| Livewire allows you to lazy load components that would otherwise slow down
|
||||
| the initial page load. Every component can have a custom placeholder or
|
||||
| you can define the default placeholder view for all components below.
|
||||
|
|
||||
*/
|
||||
|
||||
'lazy_placeholder' => null,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Temporary File Uploads
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Livewire handles file uploads by storing uploads in a temporary directory
|
||||
| before the file is stored permanently. All file uploads are directed to
|
||||
| a global endpoint for temporary storage. You may configure this below:
|
||||
|
|
||||
*/
|
||||
|
||||
'temporary_file_upload' => [
|
||||
'disk' => 'public', // Example: 'local', 's3' | Default: 'default'
|
||||
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
||||
'directory' => 'livewire-tmp', // Example: 'tmp' | Default: 'livewire-tmp'
|
||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
||||
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||
],
|
||||
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
|
||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
||||
],
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Render On Redirect
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines if Livewire will run a component's `render()` method
|
||||
| after a redirect has been triggered using something like `redirect(...)`
|
||||
| Setting this to true will render the view once more before redirecting
|
||||
|
|
||||
*/
|
||||
|
||||
'render_on_redirect' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Eloquent Model Binding
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Previous versions of Livewire supported binding directly to eloquent model
|
||||
| properties using wire:model by default. However, this behavior has been
|
||||
| deemed too "magical" and has therefore been put under a feature flag.
|
||||
|
|
||||
*/
|
||||
|
||||
'legacy_model_binding' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Auto-inject Frontend Assets
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| By default, Livewire automatically injects its JavaScript and CSS into the
|
||||
| <head> and <body> of pages containing Livewire components. By disabling
|
||||
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
||||
|
|
||||
*/
|
||||
|
||||
'inject_assets' => true,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Navigate (SPA mode)
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| By adding `wire:navigate` to links in your Livewire application, Livewire
|
||||
| will prevent the default link handling and instead request those pages
|
||||
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
||||
|
|
||||
*/
|
||||
|
||||
'navigate' => [
|
||||
'show_progress_bar' => true,
|
||||
'progress_bar_color' => '#2299dd',
|
||||
],
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| HTML Morph Markers
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
|
||||
| after each update. To make this process more reliable, Livewire injects
|
||||
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
||||
|
|
||||
*/
|
||||
|
||||
'inject_morph_markers' => true,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Smart Wire Keys
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| Livewire uses loops and keys used within loops to generate smart keys that
|
||||
| are applied to nested components that don't have them. This makes using
|
||||
| nested components more reliable by ensuring that they all have keys.
|
||||
|
|
||||
*/
|
||||
|
||||
'smart_wire_keys' => false,
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Pagination Theme
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| When enabling Livewire's pagination feature by using the `WithPagination`
|
||||
| trait, Livewire will use Tailwind templates to render pagination views
|
||||
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
||||
|
|
||||
*/
|
||||
|
||||
'pagination_theme' => 'tailwind',
|
||||
|
||||
/*
|
||||
|---------------------------------------------------------------------------
|
||||
| Release Token
|
||||
|---------------------------------------------------------------------------
|
||||
|
|
||||
| This token is stored client-side and sent along with each request to check
|
||||
| a users session to see if a new release has invalidated it. If there is
|
||||
| a mismatch it will throw an error and prompt for a browser refresh.
|
||||
|
|
||||
*/
|
||||
|
||||
'release_token' => 'a',
|
||||
];
|
||||
@@ -35,4 +35,12 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
'gemini' => [
|
||||
'key' => env('GEMINI_API_KEY'),
|
||||
],
|
||||
|
||||
'unsplash' => [
|
||||
'access_key' => env('UNSPLASH_ACCESS_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ProductCategory>
|
||||
*/
|
||||
class ProductCategoryFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
|
||||
*/
|
||||
class ProductFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('product_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('title'); // Translatable
|
||||
$table->string('slug')->unique();
|
||||
$table->foreignId('parent_id')->nullable()->constrained('product_categories')->nullOnDelete();
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('product_categories');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('products', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('title'); // Translatable
|
||||
$table->string('slug')->unique();
|
||||
$table->enum('type', ['product', 'service'])->default('product');
|
||||
$table->foreignId('product_category_id')->nullable()->constrained('product_categories')->nullOnDelete();
|
||||
$table->string('hero_image')->nullable();
|
||||
$table->json('content')->nullable(); // Translatable
|
||||
$table->string('view_template')->nullable(); // Custom view path
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('products');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('site_translations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('hash')->unique()->index(); // MD5 hash of the key
|
||||
$table->text('key'); // Original text
|
||||
$table->json('translations')->nullable(); // JSON data: {"en": "...", "tr": "..."}
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('site_translations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('imports', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->string('file_name');
|
||||
$table->string('file_path');
|
||||
$table->string('importer');
|
||||
$table->unsignedInteger('processed_rows')->default(0);
|
||||
$table->unsignedInteger('total_rows');
|
||||
$table->unsignedInteger('successful_rows')->default(0);
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('imports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('exports', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->timestamp('completed_at')->nullable();
|
||||
$table->string('file_disk');
|
||||
$table->string('file_name')->nullable();
|
||||
$table->string('exporter');
|
||||
$table->unsignedInteger('processed_rows')->default(0);
|
||||
$table->unsignedInteger('total_rows');
|
||||
$table->unsignedInteger('successful_rows')->default(0);
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('exports');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('failed_import_rows', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->json('data');
|
||||
$table->foreignId('import_id')->constrained()->cascadeOnDelete();
|
||||
$table->text('validation_error')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('failed_import_rows');
|
||||
}
|
||||
};
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->string('custom_header_blade')->nullable()->after('header_data');
|
||||
$table->string('custom_footer_blade')->nullable()->after('footer_data');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn(['custom_header_blade', 'custom_footer_blade']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->json('landing_page_data')->nullable()->after('content');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn('landing_page_data');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->string('external_url')->nullable()->after('view_template');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('products', function (Blueprint $table) {
|
||||
$table->dropColumn('external_url');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('testimonials', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('page_id')->nullable()->constrained()->onDelete('cascade');
|
||||
$table->string('name');
|
||||
$table->string('position')->nullable();
|
||||
$table->text('content');
|
||||
$table->integer('rating')->default(5);
|
||||
$table->string('avatar')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('testimonials');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('visions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('visions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->json('data')->nullable()->after('sections');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('data')->nullable();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('blog_topics', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->text('context')->nullable(); // Additional context/prompt for AI
|
||||
$table->string('status')->default('active'); // active, passive
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('blog_topics');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('career_applications', function (Blueprint $blueprint) {
|
||||
$blueprint->id();
|
||||
$blueprint->string('name');
|
||||
$blueprint->string('email');
|
||||
$blueprint->string('phone')->nullable();
|
||||
$blueprint->string('cv_path');
|
||||
$blueprint->text('message')->nullable();
|
||||
$blueprint->string('status')->default('pending'); // pending, reviewed, rejected, accepted
|
||||
$blueprint->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('career_applications');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('career_applications', function (Blueprint $table) {
|
||||
$table->string('type')->default('job')->after('phone'); // job, internship
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('career_applications', function (Blueprint $table) {
|
||||
$table->dropColumn('type');
|
||||
});
|
||||
}
|
||||
};
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('career_applications', function (Blueprint $table) {
|
||||
$table->string('nda_path')->nullable()->after('cv_path');
|
||||
$table->string('contract_path')->nullable()->after('nda_path');
|
||||
$table->boolean('git_knowledge')->default(false)->after('contract_path');
|
||||
$table->boolean('ai_usage')->default(false)->after('git_knowledge');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('career_applications', function (Blueprint $table) {
|
||||
$table->dropColumn(['nda_path', 'contract_path', 'git_knowledge', 'ai_usage']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BlogTopicSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$topics = [
|
||||
'Yapay Zeka ve İş Dünyası',
|
||||
'Mobil Uygulama Geliştirme Trendleri',
|
||||
'E-Ticaretin Geleceği',
|
||||
'Siber Güvenlik Önlemleri',
|
||||
'Blockchain Teknolojisi',
|
||||
'Bulut Bilişim Avantajları',
|
||||
'Nesnelerin İnterneti (IoT) Uygulamaları',
|
||||
'Veri Analitiği ve Büyük Veri',
|
||||
'Dijital Dönüşüm Stratejileri',
|
||||
'Web Yazılım Teknolojileri'
|
||||
];
|
||||
|
||||
foreach ($topics as $topic) {
|
||||
DB::table('blog_topics')->insert([
|
||||
'name' => $topic,
|
||||
'context' => 'Trunçgil Teknoloji çözümlerine ve uzmanlığına vurgu yapın.',
|
||||
'status' => 'active',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@ class DatabaseSeeder extends Seeder
|
||||
{
|
||||
// Settings seed
|
||||
$this->call([
|
||||
LanguageSeeder::class,
|
||||
SettingSeeder::class,
|
||||
PageSeeder::class,
|
||||
]);
|
||||
|
||||
// User::factory(10)->create();
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Page;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class LogosPageSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$author = User::first();
|
||||
if (!$author) return;
|
||||
|
||||
$corporate = Page::where('slug', 'kurumsal')->first();
|
||||
if (!$corporate) return;
|
||||
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'logolarimiz'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'parent_id' => $corporate->id,
|
||||
'title' => 'Logolarımız',
|
||||
'excerpt' => 'Kurumsal logolarımız ve kullanım standartları',
|
||||
'content' => null,
|
||||
'template' => 'corporate.logos',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 6,
|
||||
'published_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->command->info('✅ Logolarımız sayfası başarıyla eklendi!');
|
||||
}
|
||||
}
|
||||
+196
-241
@@ -4,7 +4,6 @@ namespace Database\Seeders;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PageSeeder extends Seeder
|
||||
{
|
||||
@@ -13,7 +12,7 @@ class PageSeeder extends Seeder
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// İlk kullanıcıyı bul veya oluştur (author_id için gerekli)
|
||||
// İlk kullanıcıyı bul veya oluştur
|
||||
$author = \App\Models\User::first();
|
||||
if (!$author) {
|
||||
$author = \App\Models\User::create([
|
||||
@@ -23,99 +22,172 @@ class PageSeeder extends Seeder
|
||||
]);
|
||||
}
|
||||
|
||||
$pages = [
|
||||
// Home Page
|
||||
// 1. Anasayfa
|
||||
$home = Page::updateOrCreate(
|
||||
['slug' => 'home'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'title' => 'Anasayfa',
|
||||
'slug' => 'home',
|
||||
'excerpt' => 'Modern ve yenilikçi teknoloji çözümleri',
|
||||
'content' => null,
|
||||
'template' => 'home',
|
||||
'status' => 'published',
|
||||
'is_homepage' => true,
|
||||
'show_in_menu' => false,
|
||||
'sort_order' => 1,
|
||||
'published_at' => now(),
|
||||
'meta_title' => 'Truncgil Teknoloji - Kurumsal',
|
||||
'meta_description' => 'Truncgil Teknoloji kurumsal web sitesi.',
|
||||
'sections' => [], // Sections will be handled by the template view for now or can be added here
|
||||
]
|
||||
);
|
||||
|
||||
// 2. Kurumsal (Parent)
|
||||
$corporate = Page::updateOrCreate(
|
||||
['slug' => 'kurumsal'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'title' => 'Kurumsal',
|
||||
'excerpt' => 'Kurumsal bilgilerimiz',
|
||||
'content' => null,
|
||||
'template' => 'generic',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 1,
|
||||
'published_at' => now(),
|
||||
'meta_title' => 'Truncgil Citrus - Modern Web Çözümleri',
|
||||
'meta_description' => 'Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz. Modern web uygulamaları ve dijital dönüşüm hizmetleri.',
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'background_image' => 'assets/img/photos/blurry.png',
|
||||
'badge' => 'YENİ PLATFORM',
|
||||
'title' => 'Modern ve Çok Amaçlı <span class="text-[#e31e24]">Web Çözümleri</span>',
|
||||
'subtitle' => 'Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz. Projelerinizi en son teknolojilerle hayata geçiriyoruz.',
|
||||
'primary_button_text' => 'Hemen Başla',
|
||||
'primary_button_url' => '#features',
|
||||
'secondary_button_text' => 'Daha Fazla Bilgi',
|
||||
'secondary_button_url' => '/about',
|
||||
'hero_image' => 'assets/img/demos/f1.png',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'section_badge' => 'ÖZELLİKLER',
|
||||
'section_title' => 'Neden Truncgil Citrus?',
|
||||
'section_subtitle' => 'Modern teknolojiler ve uzman ekibimizle projelerinizi hayata geçiriyoruz',
|
||||
'column_class' => 'md:w-6/12 lg:w-4/12',
|
||||
'features' => [
|
||||
[
|
||||
'icon' => 'assets/img/demos/fi1.png',
|
||||
'title' => 'Hızlı Çözümler',
|
||||
'description' => 'Modern teknolojiler kullanarak projelerinizi hızlı ve verimli bir şekilde hayata geçiriyoruz.',
|
||||
],
|
||||
[
|
||||
'icon' => 'assets/img/demos/fi2.png',
|
||||
'title' => 'Güvenli Altyapı',
|
||||
'description' => 'En son güvenlik standartlarını kullanarak verilerinizi koruma altına alıyoruz.',
|
||||
],
|
||||
[
|
||||
'icon' => 'assets/img/demos/fi3.png',
|
||||
'title' => 'Uzman Ekip',
|
||||
'description' => 'Deneyimli ve uzman ekibimiz ile her zaman yanınızdayız.',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'stats',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#ffffff]',
|
||||
'column_class' => 'md:w-6/12 lg:w-3/12',
|
||||
'stats' => [
|
||||
['number' => '500+', 'label' => 'Tamamlanan Proje'],
|
||||
['number' => '300+', 'label' => 'Mutlu Müşteri'],
|
||||
['number' => '50+', 'label' => 'Kazanılan Ödül'],
|
||||
['number' => '25+', 'label' => 'Ekip Üyesi'],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'cta',
|
||||
'data' => [
|
||||
'bg_class' => 'overflow-hidden',
|
||||
'background_image' => 'assets/img/photos/blurry.png',
|
||||
'icon' => 'assets/img/demos/icon-grape.png',
|
||||
'title' => 'Benzersiz düşünün ve <span class="text-[#e31e24]">fark yaratın</span>',
|
||||
'subtitle' => 'Binlerce müşterimiz tarafından güveniliyoruz. Siz de katılın ve projelerinizi hayata geçirin.',
|
||||
'button_text' => 'İletişime Geç',
|
||||
'button_url' => '/contact',
|
||||
'button_icon' => 'uil uil-arrow-up-right',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// About Page
|
||||
// 2.1 Hakkımızda (Child of Kurumsal)
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'hakkimizda'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'parent_id' => $corporate->id,
|
||||
'title' => 'Hakkımızda',
|
||||
'slug' => 'about',
|
||||
'excerpt' => 'Truncgil Citrus olarak yenilikçi teknoloji çözümleri sunuyoruz',
|
||||
'excerpt' => 'Biz kimiz?',
|
||||
'content' => '<p>Hakkımızda içeriği...</p>',
|
||||
'template' => 'generic',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 1,
|
||||
'published_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// 2.2 Vizyon & Misyon (Child of Kurumsal)
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'vizyon-misyon'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'parent_id' => $corporate->id,
|
||||
'title' => 'Vizyon & Misyon',
|
||||
'excerpt' => 'Gelecek hedeflerimiz',
|
||||
'content' => '<p>Vizyon ve Misyon içeriği...</p>',
|
||||
'template' => 'generic',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 2,
|
||||
'published_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// 2.3 Kitaplarımız (Child of Kurumsal)
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'kitaplarimiz'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'parent_id' => $corporate->id,
|
||||
'title' => 'Kitaplarımız',
|
||||
'excerpt' => 'Yayınladığımız kitaplar',
|
||||
'content' => null,
|
||||
'template' => 'books.books',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 3,
|
||||
'published_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// 2.4 Banka Bilgilerimiz (Child of Kurumsal)
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'banka-bilgilerimiz'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'parent_id' => $corporate->id,
|
||||
'title' => 'Banka Bilgilerimiz',
|
||||
'excerpt' => 'Banka hesap bilgilerimiz',
|
||||
'content' => '<p>Banka hesap bilgilerimiz burada yer alacaktır.</p>',
|
||||
'template' => 'generic',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 4,
|
||||
'published_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// 2.5 Müşteri Görüşleri (Child of Kurumsal)
|
||||
$testimonialsPage = Page::updateOrCreate(
|
||||
['slug' => 'musteri-gorusleri'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'parent_id' => $corporate->id,
|
||||
'title' => 'Müşteri Görüşleri',
|
||||
'excerpt' => 'Müşterilerimizin hakkımızdaki düşünceleri',
|
||||
'content' => null,
|
||||
'template' => 'corporate.testimonials',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 5,
|
||||
'published_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
// Varsayılan müşteri görüşlerini ekle
|
||||
$defaultTestimonials = [
|
||||
[
|
||||
'name' => 'Kerem Can Azak',
|
||||
'position' => 'Stellar Construction',
|
||||
'content' => 'Dijital dönüşüm yolculuğumuzda yanımızda oldukları için mutluyuz. Profesyonel yaklaşımları ve çözüm odaklı çalışmaları ile projelerimize değer kattılar.',
|
||||
'rating' => 5,
|
||||
'sort_order' => 1,
|
||||
],
|
||||
[
|
||||
'name' => 'Servet Demir',
|
||||
'position' => 'Dijimind Akademi',
|
||||
'content' => 'Eğitim teknolojileri alanındaki vizyoner bakış açıları ve teknik altyapı konusundaki uzmanlıkları sayesinde hedeflediğimiz kitleye çok daha etkili bir şekilde ulaştık.',
|
||||
'rating' => 5,
|
||||
'sort_order' => 2,
|
||||
],
|
||||
[
|
||||
'name' => 'A. Cezmi Savaş',
|
||||
'position' => 'Rhapsode Akademik Yayınevi',
|
||||
'content' => 'Akademik yayıncılık süreçlerimizi dijitalleştirirken sundukları yenilikçi çözümler ve hızlı destekleri için teşekkür ederiz. Güvenilir bir iş ortağı.',
|
||||
'rating' => 5,
|
||||
'sort_order' => 3,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($defaultTestimonials as $testimonialData) {
|
||||
\App\Models\Testimonial::updateOrCreate(
|
||||
['page_id' => $testimonialsPage->id, 'name' => $testimonialData['name']],
|
||||
$testimonialData
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Ürünlerimiz (Mega Menu Target)
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'urunlerimiz'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'title' => 'Ürünlerimiz',
|
||||
'excerpt' => 'Ürün ve hizmetlerimiz',
|
||||
'content' => null,
|
||||
'template' => 'generic',
|
||||
'status' => 'published',
|
||||
@@ -123,194 +195,77 @@ class PageSeeder extends Seeder
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 2,
|
||||
'published_at' => now(),
|
||||
'meta_title' => 'Hakkımızda - Truncgil Citrus',
|
||||
'meta_description' => 'Truncgil Citrus olarak yenilikçi teknoloji çözümleri sunuyor, dijital dönüşüm süreçlerinizde yanınızdayız.',
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'badge' => 'HAKKIMIZDA',
|
||||
'title' => 'Yenilikçi Teknoloji <span class="text-[#e31e24]">Çözümleri</span>',
|
||||
'subtitle' => '2010 yılından bu yana dijital dönüşüm süreçlerinde lider konumdayız',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#ffffff]',
|
||||
'section_title' => 'Değerlerimiz',
|
||||
'section_subtitle' => 'İş süreçlerimizi şekillendiren temel prensiplerimiz',
|
||||
'column_class' => 'md:w-6/12 lg:w-3/12',
|
||||
'features' => [
|
||||
['icon' => 'assets/img/demos/fi1.png', 'title' => 'İnovasyon', 'description' => 'Sürekli yenilik ve gelişim'],
|
||||
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Kalite', 'description' => 'En yüksek standartlarda hizmet'],
|
||||
['icon' => 'assets/img/demos/fi3.png', 'title' => 'Güvenilirlik', 'description' => 'Zamanında ve eksiksiz teslimat'],
|
||||
['icon' => 'assets/img/demos/fi4.png', 'title' => 'Destek', 'description' => '7/24 müşteri desteği'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Services Page
|
||||
// 4. Kariyer
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'kariyer'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'title' => 'Hizmetlerimiz',
|
||||
'slug' => 'services',
|
||||
'excerpt' => 'Geniş yelpazede teknoloji hizmetleri sunuyoruz',
|
||||
'content' => null,
|
||||
'title' => 'Kariyer',
|
||||
'excerpt' => 'Kariyer fırsatları',
|
||||
'content' => '<p>Kariyer içeriği...</p>',
|
||||
'template' => 'generic',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 3,
|
||||
'published_at' => now(),
|
||||
'meta_title' => 'Hizmetlerimiz - Truncgil Citrus',
|
||||
'meta_description' => 'Web tasarım, mobil uygulama geliştirme, e-ticaret çözümleri ve dijital pazarlama hizmetlerimizi keşfedin.',
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'badge' => 'HİZMETLER',
|
||||
'title' => 'Dijital Dönüşüm <span class="text-[#e31e24]">Çözümleri</span>',
|
||||
'subtitle' => 'İşinizi bir üst seviyeye taşıyacak teknoloji hizmetlerimiz',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'section_title' => 'Sunduğumuz Hizmetler',
|
||||
'column_class' => 'md:w-6/12 lg:w-4/12',
|
||||
'features' => [
|
||||
['icon' => 'assets/img/demos/fi1.png', 'title' => 'Web Tasarım & Geliştirme', 'description' => 'Modern, responsive ve SEO uyumlu web siteleri'],
|
||||
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Mobil Uygulama', 'description' => 'iOS ve Android için native ve cross-platform uygulamalar'],
|
||||
['icon' => 'assets/img/demos/fi3.png', 'title' => 'E-Ticaret Çözümleri', 'description' => 'Entegre ödeme sistemleri ile online satış platformları'],
|
||||
['icon' => 'assets/img/demos/fi4.png', 'title' => 'Dijital Pazarlama', 'description' => 'SEO, SEM ve sosyal medya yönetimi'],
|
||||
['icon' => 'assets/img/demos/fi1.png', 'title' => 'Kurumsal Yazılım', 'description' => 'İşletmenize özel CRM, ERP ve özel yazılım çözümleri'],
|
||||
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Bulut Hizmetleri', 'description' => 'Güvenli ve ölçeklenebilir bulut altyapısı'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Contact Page
|
||||
// 5. Neler Yaparız
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'neler-yapariz'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'title' => 'İletişim',
|
||||
'slug' => 'contact',
|
||||
'excerpt' => 'Projeleriniz hakkında bizimle iletişime geçin',
|
||||
'title' => 'Neler Yaparız',
|
||||
'excerpt' => 'Hizmetlerimiz ve çözümlerimiz',
|
||||
'content' => null,
|
||||
'template' => 'generic',
|
||||
'template' => 'neler-yapariz',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 4,
|
||||
'published_at' => now(),
|
||||
'meta_title' => 'İletişim - Truncgil Citrus',
|
||||
'meta_description' => 'Projeleriniz hakkında konuşmak için bizimle iletişime geçin. Uzman ekibimiz size yardımcı olmaktan mutluluk duyar.',
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'badge' => 'İLETİŞİM',
|
||||
'title' => 'Projelerinizi <span class="text-[#e31e24]">Konuşalım</span>',
|
||||
'subtitle' => 'Size özel çözümler geliştirmek için bizimle iletişime geçin',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'contact',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'title' => 'Bizimle İletişime Geçin',
|
||||
'subtitle' => 'Projeleriniz hakkında konuşmak için bize ulaşın',
|
||||
'info' => [
|
||||
'address' => 'Merkez Mah. Teknoloji Cad. No:123 İstanbul',
|
||||
'phone' => '+90 (212) 555 1234',
|
||||
'email' => 'info@truncgil.com',
|
||||
],
|
||||
'form_action' => '/contact/submit',
|
||||
'button_text' => 'Gönder',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
// Pricing Page (örnek)
|
||||
// 6. Blog
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'blog'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'title' => 'Fiyatlandırma',
|
||||
'slug' => 'pricing',
|
||||
'excerpt' => 'Size uygun paketi seçin',
|
||||
'title' => 'Blog',
|
||||
'excerpt' => 'Güncel haberler ve makaleler',
|
||||
'content' => null,
|
||||
'template' => 'generic',
|
||||
'template' => 'blog',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 5,
|
||||
'published_at' => now(),
|
||||
'meta_title' => 'Fiyatlandırma - Truncgil Citrus',
|
||||
'meta_description' => 'İhtiyaçlarınıza uygun web tasarım ve yazılım geliştirme paketlerimizi inceleyin.',
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'badge' => 'FİYATLANDIRMA',
|
||||
'title' => 'Size Uygun <span class="text-[#e31e24]">Paketi Seçin</span>',
|
||||
'subtitle' => 'Tüm paketler 30 gün para iade garantisi ile geliyor',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'pricing',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'section_title' => 'Web Tasarım Paketleri',
|
||||
'column_class' => 'md:w-6/12 lg:w-4/12',
|
||||
'featured_label' => 'Önerilen',
|
||||
'plans' => [
|
||||
[
|
||||
'name' => 'Başlangıç',
|
||||
'currency' => '₺',
|
||||
'price' => '999',
|
||||
'period' => 'ay',
|
||||
'features' => ['5 Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', '7/24 Destek'],
|
||||
'button_text' => 'Başla',
|
||||
'button_url' => '/contact',
|
||||
],
|
||||
[
|
||||
'name' => 'Profesyonel',
|
||||
'currency' => '₺',
|
||||
'price' => '2499',
|
||||
'period' => 'ay',
|
||||
'featured' => true,
|
||||
'features' => ['15 Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', 'Yönetim Paneli', '7/24 Öncelikli Destek'],
|
||||
'button_text' => 'Başla',
|
||||
'button_url' => '/contact',
|
||||
],
|
||||
[
|
||||
'name' => 'Kurumsal',
|
||||
'currency' => '₺',
|
||||
'price' => '4999',
|
||||
'period' => 'ay',
|
||||
'features' => ['Sınırsız Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', 'Gelişmiş Yönetim Paneli', 'Özel Entegrasyonlar', '7/24 Öncelikli Destek'],
|
||||
'button_text' => 'Başla',
|
||||
'button_url' => '/contact',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($pages as $pageData) {
|
||||
Page::updateOrCreate(
|
||||
['slug' => $pageData['slug']],
|
||||
$pageData
|
||||
);
|
||||
}
|
||||
// 7. İletişim
|
||||
Page::updateOrCreate(
|
||||
['slug' => 'iletisim'],
|
||||
[
|
||||
'author_id' => $author->id,
|
||||
'title' => 'İletişim',
|
||||
'excerpt' => 'Bize ulaşın',
|
||||
'content' => null,
|
||||
'template' => 'contact',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
'show_in_menu' => true,
|
||||
'sort_order' => 6,
|
||||
'published_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
$this->command->info('✅ ' . count($pages) . ' sayfa başarıyla oluşturuldu!');
|
||||
$this->command->info('✅ Menu yapısı ve sayfalar başarıyla oluşturuldu!');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,6 +659,16 @@ class SettingSeeder extends Seeder
|
||||
'is_public' => false,
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'key' => 'seo_google_search_console',
|
||||
'value' => '',
|
||||
'type' => 'string',
|
||||
'group' => 'seo',
|
||||
'label' => 'Google Search Console',
|
||||
'description' => 'Google Search Console doğrulama kodu',
|
||||
'is_public' => false,
|
||||
'is_active' => true,
|
||||
],
|
||||
[
|
||||
'key' => 'seo_google_tag_manager_id',
|
||||
'value' => '',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user