Add Page Section Builder functionality: Introduced a new section builder for dynamic page sections in the Filament Admin panel, allowing users to add various section types with flexible key-value data management. Enhanced the Page model with methods for parsed sections and section data retrieval. Updated localization files to support new section-related terms and added comprehensive documentation for usage and best practices.
This commit is contained in:
+115
@@ -210,6 +210,121 @@ $table
|
||||
|
||||
**Önemli Not:** Filament 4.x'te `Filament\Resources\Components\Tab` diye bir class **YOK**. Tüm tab kullanımları `Filament\Schemas\Components\Tabs\Tab` ile yapılır!
|
||||
|
||||
#### **Filament 4.x REPEATER KULLANIM KURALLARI (KRİTİK!):**
|
||||
|
||||
Repeater component'ı JSON array formatında tekrarlanan form component'leri oluşturmak için kullanılır.
|
||||
|
||||
**Temel Kullanım:**
|
||||
```php
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Select;
|
||||
|
||||
Repeater::make('members')
|
||||
->schema([
|
||||
TextInput::make('name')->required(),
|
||||
Select::make('role')
|
||||
->options([
|
||||
'member' => 'Member',
|
||||
'administrator' => 'Administrator',
|
||||
])
|
||||
->required(),
|
||||
])
|
||||
->defaultItems(0)
|
||||
->addActionLabel('Add Member')
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): ?string => $state['name'] ?? null)
|
||||
```
|
||||
|
||||
**Simple Repeater (Tek Field için):**
|
||||
```php
|
||||
// ✅ DOĞRU - Filament 4.x'te simple() metodu parametre alır
|
||||
Repeater::make('tags')
|
||||
->simple(
|
||||
TextInput::make('tag')
|
||||
->label('Tag')
|
||||
->required()
|
||||
)
|
||||
|
||||
// ❌ YANLIŞ - Bu kullanım HATA VERİR
|
||||
Repeater::make('tags')
|
||||
->schema([
|
||||
TextInput::make('tag')->required(),
|
||||
])
|
||||
->simple() // ❌ Parametre gerekli!
|
||||
```
|
||||
|
||||
**Nested Repeater ile Dikkat Edilmesi Gerekenler:**
|
||||
|
||||
1. **Aynı İsimli Field'lar Çakışır:**
|
||||
```php
|
||||
// ❌ YANLIŞ - Bu HATA VERİR (FileUpload array beklerken string alır)
|
||||
Repeater::make('data')
|
||||
->schema([
|
||||
TextInput::make('value'), // İlk value
|
||||
FileUpload::make('value'), // İkinci value - ÇAKIŞMA!
|
||||
Textarea::make('value'), // Üçüncü value - ÇAKIŞMA!
|
||||
])
|
||||
|
||||
// ✅ DOĞRU - dehydrated() ile sadece görünür olan kaydedilir
|
||||
Repeater::make('data')
|
||||
->schema([
|
||||
TextInput::make('value')
|
||||
->visible(fn ($get) => $get('type') === 'text')
|
||||
->dehydrated(fn ($get) => $get('type') === 'text'),
|
||||
|
||||
FileUpload::make('value')
|
||||
->visible(fn ($get) => $get('type') === 'image')
|
||||
->dehydrated(fn ($get) => $get('type') === 'image'),
|
||||
|
||||
Textarea::make('value')
|
||||
->visible(fn ($get) => $get('type') === 'textarea')
|
||||
->dehydrated(fn ($get) => $get('type') === 'textarea'),
|
||||
])
|
||||
```
|
||||
|
||||
2. **Conditional Field'lar:**
|
||||
```php
|
||||
// Sadece görünür olan field dehydrate edilir
|
||||
Select::make('type')
|
||||
->options([...])
|
||||
->live(), // Değişikliği dinlemek için
|
||||
|
||||
TextInput::make('value')
|
||||
->visible(fn (Get $get) => $get('type') === 'text')
|
||||
->dehydrated(fn (Get $get) => $get('type') === 'text'),
|
||||
```
|
||||
|
||||
3. **Item Labels:**
|
||||
```php
|
||||
// Item'lara dinamik label ekle
|
||||
Repeater::make('sections')
|
||||
->schema([...])
|
||||
->itemLabel(fn (array $state): ?string =>
|
||||
isset($state['type'])
|
||||
? __('pages.section_type_' . $state['type'])
|
||||
: 'Item'
|
||||
)
|
||||
```
|
||||
|
||||
4. **Repeater Özellikleri:**
|
||||
```php
|
||||
Repeater::make('items')
|
||||
->schema([...])
|
||||
->defaultItems(0) // Başlangıç item sayısı
|
||||
->addActionLabel('Add') // Ekleme butonu label'ı
|
||||
->reorderable() // Sürükle-bırak ile sıralama
|
||||
->collapsible() // Açılıp kapanabilir
|
||||
->collapsed() // Başlangıçta kapalı
|
||||
->cloneable() // Kopyalama butonu
|
||||
->deletable() // Silme butonu
|
||||
->minItems(1) // Minimum item sayısı
|
||||
->maxItems(10) // Maximum item sayısı
|
||||
```
|
||||
|
||||
**Referans:** [Filament Repeater Docs](https://filamentphp.com/docs/4.x/forms/repeater)
|
||||
|
||||
### Dil Dosyası Yapısı
|
||||
<?php
|
||||
|
||||
|
||||
@@ -4,14 +4,20 @@ namespace App\Filament\Admin\Resources\Pages\Schemas;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\MarkdownEditor;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class PageForm
|
||||
@@ -164,6 +170,229 @@ class PageForm
|
||||
->collapsible(true)
|
||||
->collapsed(true),
|
||||
|
||||
// Sayfa Bölümleri (Section Builder)
|
||||
Section::make(__('pages.form_section_sections'))
|
||||
->schema([
|
||||
Repeater::make('sections')
|
||||
->label(__('pages.sections_field'))
|
||||
->helperText(__('pages.sections_helper_text'))
|
||||
->schema([
|
||||
Select::make('type')
|
||||
->label(__('pages.section_type'))
|
||||
->helperText(__('pages.section_type_helper'))
|
||||
->required()
|
||||
->options([
|
||||
'hero' => __('pages.section_type_hero'),
|
||||
'features' => __('pages.section_type_features'),
|
||||
'stats' => __('pages.section_type_stats'),
|
||||
'cta' => __('pages.section_type_cta'),
|
||||
'content' => __('pages.section_type_content'),
|
||||
'gallery' => __('pages.section_type_gallery'),
|
||||
'testimonials' => __('pages.section_type_testimonials'),
|
||||
'team' => __('pages.section_type_team'),
|
||||
'pricing' => __('pages.section_type_pricing'),
|
||||
'faq' => __('pages.section_type_faq'),
|
||||
'contact' => __('pages.section_type_contact'),
|
||||
'custom' => __('pages.section_type_custom'),
|
||||
])
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
Repeater::make('data')
|
||||
->label(__('pages.section_data'))
|
||||
->helperText(__('pages.section_data_helper'))
|
||||
->schema([
|
||||
TextInput::make('key')
|
||||
->label(__('pages.section_data_key'))
|
||||
->helperText(__('pages.section_data_key_helper'))
|
||||
->required()
|
||||
->placeholder('title, subtitle, image, button_text, ...')
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('type')
|
||||
->label(__('pages.section_data_value_type'))
|
||||
->required()
|
||||
->options([
|
||||
'text' => __('pages.value_type_text'),
|
||||
'textarea' => __('pages.value_type_textarea'),
|
||||
'html' => __('pages.value_type_html'),
|
||||
'markdown' => __('pages.value_type_markdown'),
|
||||
'richtext' => __('pages.value_type_richtext'),
|
||||
'image' => __('pages.value_type_image'),
|
||||
'file' => __('pages.value_type_file'),
|
||||
'url' => __('pages.value_type_url'),
|
||||
'email' => __('pages.value_type_email'),
|
||||
'phone' => __('pages.value_type_phone'),
|
||||
'number' => __('pages.value_type_number'),
|
||||
'boolean' => __('pages.value_type_boolean'),
|
||||
'color' => __('pages.value_type_color'),
|
||||
'date' => __('pages.value_type_date'),
|
||||
'datetime' => __('pages.value_type_datetime'),
|
||||
'array' => __('pages.value_type_array'),
|
||||
'json' => __('pages.value_type_json'),
|
||||
])
|
||||
->live()
|
||||
->columnSpan(1),
|
||||
|
||||
// Single Hidden Field to store the actual value
|
||||
Hidden::make('value'),
|
||||
|
||||
// Text Input (displays and updates 'value' field)
|
||||
TextInput::make('_value_text')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => in_array($get('type'), ['text', 'url', 'email', 'phone', 'number']))
|
||||
->numeric(fn (Get $get) => $get('type') === 'number')
|
||||
->placeholder('Değer girin...')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Textarea (displays and updates 'value' field)
|
||||
Textarea::make('_value_textarea')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => in_array($get('type'), ['textarea', 'json', 'html']))
|
||||
->rows(fn (Get $get) => $get('type') === 'html' ? 6 : 4)
|
||||
->placeholder(fn (Get $get) => $get('type') === 'html' ? '<div>HTML kodu girin...</div>' : 'Metin girin...')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Rich Text Editor (displays and updates 'value' field)
|
||||
RichEditor::make('_value_richtext')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'richtext')
|
||||
->fileAttachmentsDisk('public')
|
||||
->fileAttachmentsDirectory('pages/sections')
|
||||
->toolbarButtons([
|
||||
'bold',
|
||||
'italic',
|
||||
'link',
|
||||
'bulletList',
|
||||
'orderedList',
|
||||
])
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Markdown Editor (displays and updates 'value' field)
|
||||
MarkdownEditor::make('_value_markdown')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'markdown')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Image Upload (displays and updates 'value' field)
|
||||
FileUpload::make('_value_image')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'image')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('pages/sections')
|
||||
->imageEditor()
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// File Upload (displays and updates 'value' field)
|
||||
FileUpload::make('_value_file')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'file')
|
||||
->disk('public')
|
||||
->directory('pages/sections')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Boolean Toggle (displays and updates 'value' field)
|
||||
Toggle::make('_value_boolean')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'boolean')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Color Picker (displays and updates 'value' field)
|
||||
ColorPicker::make('_value_color')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'color')
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Date Picker (displays and updates 'value' field)
|
||||
DatePicker::make('_value_date')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'date')
|
||||
->displayFormat('d/m/Y')
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// DateTime Picker (displays and updates 'value' field)
|
||||
DateTimePicker::make('_value_datetime')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'datetime')
|
||||
->displayFormat('d/m/Y H:i')
|
||||
->seconds(false)
|
||||
->live()
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
|
||||
// Array Repeater (displays and updates 'value' field)
|
||||
Repeater::make('_value_array')
|
||||
->label(__('pages.section_data_value'))
|
||||
->visible(fn (Get $get) => $get('type') === 'array')
|
||||
->simple(
|
||||
TextInput::make('item')
|
||||
->label('Öğe')
|
||||
->required()
|
||||
)
|
||||
->live(onBlur: true)
|
||||
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
|
||||
->afterStateHydrated(fn ($component, $state, Get $get) => $component->state($get('value')))
|
||||
->dehydrated(false)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columns(2)
|
||||
->defaultItems(0)
|
||||
->addActionLabel(__('pages.section_data_add_field'))
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): ?string => isset($state['key']) ? $state['key'] . ' (' . ($state['type'] ?? 'text') . ')' : null)
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->defaultItems(0)
|
||||
->addActionLabel(__('pages.section_add'))
|
||||
->reorderable()
|
||||
->collapsible()
|
||||
->itemLabel(fn (array $state): ?string => isset($state['type']) ? __('pages.section_type_' . $state['type']) : 'Bölüm')
|
||||
->columnSpanFull(),
|
||||
])
|
||||
->columnSpanFull()
|
||||
->collapsible(true)
|
||||
->collapsed(false),
|
||||
|
||||
// Çeviri Sekmesi
|
||||
Section::make('🌍 ' . __('pages.translations_section'))
|
||||
->schema([
|
||||
|
||||
@@ -92,7 +92,8 @@ class PageController extends Controller
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
$sections = $page->sections ?? $page->data ?? [];
|
||||
// Use parsed_sections for easier template usage (key-value format)
|
||||
$sections = $page->parsed_sections ?? $page->sections ?? $page->data ?? [];
|
||||
$template = ($slug === 'home' || ($page->is_homepage ?? false))
|
||||
? 'home'
|
||||
: ($page->template ?? 'generic');
|
||||
|
||||
@@ -88,4 +88,40 @@ class Page extends Model
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sections with parsed data as associative array
|
||||
*/
|
||||
public function getParsedSectionsAttribute()
|
||||
{
|
||||
if (!$this->sections || !is_array($this->sections)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($this->sections)->map(function ($section) {
|
||||
if (!isset($section['data']) || !is_array($section['data'])) {
|
||||
return $section;
|
||||
}
|
||||
|
||||
// Convert key-value-type array to associative array
|
||||
$parsedData = collect($section['data'])->pluck('value', 'key')->toArray();
|
||||
|
||||
return [
|
||||
'type' => $section['type'] ?? null,
|
||||
'data' => $parsedData,
|
||||
];
|
||||
})->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific section's data by type
|
||||
*/
|
||||
public function getSectionData(string $type, int $index = 0): ?array
|
||||
{
|
||||
$sections = $this->parsed_sections;
|
||||
$matchingSections = array_filter($sections, fn($s) => ($s['type'] ?? null) === $type);
|
||||
$matchingSections = array_values($matchingSections);
|
||||
|
||||
return $matchingSections[$index] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
# Page Section Builder - Kullanım Kılavuzu
|
||||
|
||||
## 📋 Genel Bakış
|
||||
|
||||
Page Section Builder, Filament Admin panelinden sayfalarınıza dinamik bölümler (sections) eklemenizi sağlayan güçlü bir modüldür. Settings modülünden esinlenerek geliştirilmiş olup, esnek bir key-value-type sistemi kullanır.
|
||||
|
||||
## ✨ Özellikler
|
||||
|
||||
- **Dinamik Section Tipleri**: Hero, Features, Stats, CTA, Gallery, Testimonials ve daha fazlası
|
||||
- **Esnek Data Yönetimi**: Her section için sınırsız key-value çifti
|
||||
- **Çoklu Value Tipleri**: Text, Image, HTML, Rich Text, Markdown, Color, Date, Array ve daha fazlası
|
||||
- **Sürükle-Bırak Sıralama**: Sections ve data fields'ları yeniden sıralayabilme
|
||||
- **Kolay Entegrasyon**: Mevcut sayfalarda sorunsuz çalışır
|
||||
|
||||
## 🎯 Section Tipleri
|
||||
|
||||
Aşağıdaki section tipleri mevcuttur:
|
||||
|
||||
| Tip | Açıklama | Kullanım Alanı |
|
||||
|-----|----------|----------------|
|
||||
| `hero` | Hero (Ana Banner) | Sayfa başlığı, büyük görsel banner |
|
||||
| `features` | Özellikler | Ürün/hizmet özellikleri listesi |
|
||||
| `stats` | İstatistikler | Sayısal veriler, başarı metrikleri |
|
||||
| `cta` | Harekete Geçirme | Kullanıcıyı yönlendirme butonları |
|
||||
| `content` | İçerik | Genel içerik blokları |
|
||||
| `gallery` | Galeri | Görsel galeri |
|
||||
| `testimonials` | Referanslar | Müşteri yorumları |
|
||||
| `team` | Ekip | Ekip üyeleri |
|
||||
| `pricing` | Fiyatlandırma | Fiyat tabloları |
|
||||
| `faq` | SSS | Sık sorulan sorular |
|
||||
| `contact` | İletişim | İletişim formu |
|
||||
| `custom` | Özel | Özel içerik |
|
||||
|
||||
## 📝 Value Tipleri
|
||||
|
||||
Her data field'ı için aşağıdaki value tipleri kullanılabilir:
|
||||
|
||||
### Metin Tipleri
|
||||
- **Text**: Kısa tek satırlık metin (başlık, URL, e-posta, telefon)
|
||||
- **Textarea**: Uzun metin
|
||||
- **HTML**: Ham HTML kodu
|
||||
- **Markdown**: Markdown formatı
|
||||
- **Rich Text**: WYSIWYG zengin metin editörü
|
||||
|
||||
### Medya Tipleri
|
||||
- **Image**: Görsel yükleme (otomatik image editor ile)
|
||||
- **File**: Dosya yükleme
|
||||
- **Color**: Renk seçici
|
||||
|
||||
### Veri Tipleri
|
||||
- **Number**: Sayısal değer
|
||||
- **Boolean**: Evet/Hayır (Toggle)
|
||||
- **Date**: Tarih seçici
|
||||
- **DateTime**: Tarih & saat seçici
|
||||
- **Array**: Dizi (repeater)
|
||||
- **JSON**: JSON formatı
|
||||
|
||||
## 🔧 Admin Panelinde Kullanım
|
||||
|
||||
### 1. Yeni Section Ekleme
|
||||
|
||||
1. **Pages** modülünden bir sayfa açın veya yeni sayfa oluşturun
|
||||
2. **Sayfa Bölümleri** (Page Sections) sekmesine gidin
|
||||
3. **Yeni Bölüm Ekle** butonuna tıklayın
|
||||
4. Section tipini seçin (örn: Hero, Features)
|
||||
5. **Yeni Alan Ekle** ile section data'larını ekleyin
|
||||
|
||||
### 2. Data Field Ekleme
|
||||
|
||||
Her section için data field'ları eklerken:
|
||||
|
||||
1. **Key (Anahtar)**: Data'nın anahtarı (örn: `title`, `subtitle`, `image`)
|
||||
2. **Type (Değer Tipi)**: Değerin tipini seçin (örn: Text, Image, HTML)
|
||||
3. **Value (Değer)**: Seçilen tipe göre değeri girin
|
||||
|
||||
### 3. Örnek: Hero Section Oluşturma
|
||||
|
||||
```
|
||||
Section Type: Hero (Ana Banner)
|
||||
|
||||
Data Fields:
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Key: background_image │
|
||||
│ Type: Image │
|
||||
│ Value: [Upload Image] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: badge │
|
||||
│ Type: Text │
|
||||
│ Value: "YENİ PLATFORM" │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: title │
|
||||
│ Type: HTML │
|
||||
│ Value: "Modern <span>Web</span>" │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: subtitle │
|
||||
│ Type: Textarea │
|
||||
│ Value: "Açıklama metni..." │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: button_text │
|
||||
│ Type: Text │
|
||||
│ Value: "Hemen Başla" │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: button_url │
|
||||
│ Type: URL │
|
||||
│ Value: "#features" │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 💻 Kod Kullanımı
|
||||
|
||||
### Page Model'de Sections Kullanımı
|
||||
|
||||
```php
|
||||
// Bir sayfayı al
|
||||
$page = Page::find(1);
|
||||
|
||||
// Tüm parsed sections'ları al (key-value formatında)
|
||||
$sections = $page->parsed_sections;
|
||||
|
||||
// Örnek çıktı:
|
||||
[
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'background_image' => 'path/to/image.jpg',
|
||||
'badge' => 'YENİ PLATFORM',
|
||||
'title' => 'Modern <span>Web</span>',
|
||||
'subtitle' => 'Açıklama metni...',
|
||||
'button_text' => 'Hemen Başla',
|
||||
'button_url' => '#features',
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [...]
|
||||
]
|
||||
]
|
||||
|
||||
// Belirli bir section tipinin data'sını al
|
||||
$heroData = $page->getSectionData('hero'); // İlk hero section
|
||||
$secondHero = $page->getSectionData('hero', 1); // İkinci hero section
|
||||
```
|
||||
|
||||
### Controller'da Kullanım
|
||||
|
||||
```php
|
||||
public function show($slug)
|
||||
{
|
||||
$page = Page::where('slug', $slug)->firstOrFail();
|
||||
|
||||
// Parsed sections otomatik olarak view'e gönderilir
|
||||
$sections = $page->parsed_sections;
|
||||
|
||||
return view('templates.home', compact('page', 'sections'));
|
||||
}
|
||||
```
|
||||
|
||||
### Blade Template'de Kullanım
|
||||
|
||||
```blade
|
||||
@foreach($sections as $section)
|
||||
@if($section['type'] === 'hero')
|
||||
<x-blocks.hero :data="$section['data']" />
|
||||
|
||||
@elseif($section['type'] === 'features')
|
||||
<x-blocks.features :data="$section['data']" />
|
||||
|
||||
@elseif($section['type'] === 'stats')
|
||||
<x-blocks.stats :data="$section['data']" />
|
||||
|
||||
@endif
|
||||
@endforeach
|
||||
```
|
||||
|
||||
### Component'te Data Kullanımı
|
||||
|
||||
```blade
|
||||
{{-- resources/views/components/blocks/hero.blade.php --}}
|
||||
|
||||
@props(['data'])
|
||||
|
||||
<section style="background-image: url({{ asset($data['background_image'] ?? '') }})">
|
||||
<div class="container">
|
||||
@if(isset($data['badge']))
|
||||
<span class="badge">{{ $data['badge'] }}</span>
|
||||
@endif
|
||||
|
||||
@if(isset($data['title']))
|
||||
<h1>{!! $data['title'] !!}</h1>
|
||||
@endif
|
||||
|
||||
@if(isset($data['subtitle']))
|
||||
<p>{{ $data['subtitle'] }}</p>
|
||||
@endif
|
||||
|
||||
@if(isset($data['button_text']) && isset($data['button_url']))
|
||||
<a href="{{ $data['button_url'] }}" class="btn">
|
||||
{{ $data['button_text'] }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
## 🎨 Best Practices
|
||||
|
||||
### 1. Key Naming (Anahtar İsimlendirme)
|
||||
|
||||
Tutarlı ve açıklayıcı key isimleri kullanın:
|
||||
|
||||
```
|
||||
✅ İYİ:
|
||||
- title
|
||||
- subtitle
|
||||
- background_image
|
||||
- primary_button_text
|
||||
- primary_button_url
|
||||
|
||||
❌ KÖTÜ:
|
||||
- baslik
|
||||
- resim1
|
||||
- text
|
||||
- btn
|
||||
```
|
||||
|
||||
### 2. Value Type Seçimi
|
||||
|
||||
Doğru value tipini seçin:
|
||||
|
||||
```
|
||||
✅ title → Text (kısa başlık)
|
||||
✅ description → Textarea (uzun açıklama)
|
||||
✅ content → Rich Text (formatlanmış içerik)
|
||||
✅ banner → Image (görsel)
|
||||
✅ is_active → Boolean (aktif/pasif)
|
||||
```
|
||||
|
||||
### 3. Section Organization
|
||||
|
||||
Benzer section'ları gruplandırın:
|
||||
|
||||
```
|
||||
1. Hero (Giriş)
|
||||
2. Features (Özellikler)
|
||||
3. Stats (İstatistikler)
|
||||
4. CTA (Harekete Geçirme)
|
||||
5. Contact (İletişim)
|
||||
```
|
||||
|
||||
## 🔄 Mevcut Data Migrasyon
|
||||
|
||||
Eğer eski formatınız varsa (örn: PageSeeder'daki gibi), parse edilmiş format otomatik olarak çalışır:
|
||||
|
||||
```php
|
||||
// ESKİ FORMAT (PageSeeder'dan)
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'title' => 'Başlık',
|
||||
'subtitle' => 'Alt başlık'
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
// YENİ FORMAT (Admin'den girilince)
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
['key' => 'title', 'type' => 'text', 'value' => 'Başlık'],
|
||||
['key' => 'subtitle', 'type' => 'text', 'value' => 'Alt başlık']
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
// parsed_sections attribute her ikisini de doğru parse eder!
|
||||
```
|
||||
|
||||
## 🚀 İleri Seviye Özellikler
|
||||
|
||||
### Nested Arrays (İç İçe Diziler)
|
||||
|
||||
`Array` value tipi ile iç içe veri yapıları oluşturabilirsiniz:
|
||||
|
||||
```
|
||||
Section Type: Features
|
||||
|
||||
Data Field:
|
||||
Key: features
|
||||
Type: Array
|
||||
Value: [
|
||||
{ item: "Hızlı Çözümler" },
|
||||
{ item: "Güvenli Altyapı" },
|
||||
{ item: "Uzman Ekip" }
|
||||
]
|
||||
```
|
||||
|
||||
### JSON Data
|
||||
|
||||
Kompleks veri yapıları için JSON kullanabilirsiniz:
|
||||
|
||||
```
|
||||
Key: configuration
|
||||
Type: JSON
|
||||
Value: {"layout": "grid", "columns": 3, "gap": 20}
|
||||
```
|
||||
|
||||
### Conditional Rendering
|
||||
|
||||
```blade
|
||||
@foreach($sections as $section)
|
||||
@php
|
||||
$data = $section['data'];
|
||||
$bgClass = $data['bg_class'] ?? '';
|
||||
@endphp
|
||||
|
||||
<section class="{{ $bgClass }}">
|
||||
@includeIf("components.blocks.{$section['type']}", compact('data'))
|
||||
</section>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
## 📚 Dosya Yapısı
|
||||
|
||||
```
|
||||
app/
|
||||
├── Models/
|
||||
│ └── Page.php (getParsedSectionsAttribute, getSectionData)
|
||||
├── Http/Controllers/
|
||||
│ └── PageController.php (parsed_sections kullanımı)
|
||||
└── Filament/Admin/Resources/Pages/
|
||||
└── Schemas/
|
||||
└── PageForm.php (Section Builder UI)
|
||||
|
||||
lang/
|
||||
├── tr/
|
||||
│ └── pages.php (Türkçe çeviriler)
|
||||
└── en/
|
||||
└── pages.php (İngilizce çeviriler)
|
||||
|
||||
resources/views/
|
||||
├── components/blocks/
|
||||
│ ├── hero.blade.php
|
||||
│ ├── features.blade.php
|
||||
│ ├── stats.blade.php
|
||||
│ └── cta.blade.php
|
||||
└── templates/
|
||||
└── home.blade.php (sections render)
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Section görünmüyor?
|
||||
|
||||
1. `$page->parsed_sections` döndüğünden emin olun
|
||||
2. Section type'ının doğru yazıldığını kontrol edin
|
||||
3. Blade component'in var olduğunu doğrulayın
|
||||
|
||||
### Data değerleri boş?
|
||||
|
||||
1. Admin'de data field'larının key değerlerini kontrol edin
|
||||
2. Value tipinin doğru seçildiğini doğrulayın
|
||||
3. Değerlerin kaydedildiğini kontrol edin
|
||||
|
||||
### Image görünmüyor?
|
||||
|
||||
1. `storage` link'inin oluşturulduğunu kontrol edin: `php artisan storage:link`
|
||||
2. Image path'inin doğru olduğunu doğrulayın
|
||||
3. Disk ayarlarını kontrol edin (`config/filesystems.php`)
|
||||
|
||||
## 🎓 Örnekler
|
||||
|
||||
Daha fazla örnek için:
|
||||
- `database/seeders/PageSeeder.php` - Mevcut section örnekleri
|
||||
- `resources/views/components/blocks/` - Component örnekleri
|
||||
- `resources/views/templates/home.blade.php` - Template örneği
|
||||
|
||||
## 📞 Destek
|
||||
|
||||
Sorunlarınız için:
|
||||
1. Bu dokümantasyonu kontrol edin
|
||||
2. `docs/CURSOR_RULES.md` dosyasına bakın
|
||||
3. Loglara göz atın: `storage/logs/laravel.log`
|
||||
|
||||
---
|
||||
|
||||
**Not:** Bu sistem Settings modülünden esinlenerek geliştirilmiş olup, aynı esneklik ve güçle çalışır. Her türlü dinamik sayfa içeriği için kullanılabilir.
|
||||
|
||||
@@ -24,6 +24,7 @@ return [
|
||||
'form_section_content' => 'Content',
|
||||
'form_section_page_settings' => 'Page Settings',
|
||||
'form_section_seo_settings' => 'SEO Settings',
|
||||
'form_section_sections' => 'Page Sections',
|
||||
'translations_section' => 'Translations',
|
||||
|
||||
// Form fields
|
||||
@@ -95,4 +96,53 @@ return [
|
||||
'slug_required' => 'The URL slug field is required.',
|
||||
'slug_unique' => 'This URL slug is already taken.',
|
||||
'content_required' => 'The content field is required.',
|
||||
|
||||
// Section Builder
|
||||
'sections_field' => 'Page Sections',
|
||||
'sections_helper_text' => 'Add dynamic sections to your page',
|
||||
'section_type' => 'Section Type',
|
||||
'section_type_helper' => 'Select the type of section you want to add',
|
||||
'section_data' => 'Section Data',
|
||||
'section_data_helper' => 'Enter the required data for this section',
|
||||
'section_add' => 'Add New Section',
|
||||
|
||||
// Section Types
|
||||
'section_type_hero' => 'Hero (Main Banner)',
|
||||
'section_type_features' => 'Features',
|
||||
'section_type_stats' => 'Statistics',
|
||||
'section_type_cta' => 'Call to Action',
|
||||
'section_type_content' => 'Content',
|
||||
'section_type_gallery' => 'Gallery',
|
||||
'section_type_testimonials' => 'Testimonials',
|
||||
'section_type_team' => 'Team',
|
||||
'section_type_pricing' => 'Pricing',
|
||||
'section_type_faq' => 'FAQ',
|
||||
'section_type_contact' => 'Contact',
|
||||
'section_type_custom' => 'Custom',
|
||||
|
||||
// Section Data Fields
|
||||
'section_data_key' => 'Key',
|
||||
'section_data_key_helper' => 'Data key (e.g: title, subtitle, image)',
|
||||
'section_data_value_type' => 'Value Type',
|
||||
'section_data_value' => 'Value',
|
||||
'section_data_add_field' => 'Add New Field',
|
||||
|
||||
// Value Types
|
||||
'value_type_text' => 'Text',
|
||||
'value_type_textarea' => 'Long Text',
|
||||
'value_type_html' => 'HTML',
|
||||
'value_type_markdown' => 'Markdown',
|
||||
'value_type_richtext' => 'Rich Text',
|
||||
'value_type_image' => 'Image',
|
||||
'value_type_file' => 'File',
|
||||
'value_type_url' => 'URL',
|
||||
'value_type_email' => 'Email',
|
||||
'value_type_phone' => 'Phone',
|
||||
'value_type_number' => 'Number',
|
||||
'value_type_boolean' => 'Yes/No',
|
||||
'value_type_color' => 'Color',
|
||||
'value_type_date' => 'Date',
|
||||
'value_type_datetime' => 'Date & Time',
|
||||
'value_type_array' => 'Array',
|
||||
'value_type_json' => 'JSON',
|
||||
];
|
||||
|
||||
@@ -24,6 +24,7 @@ return [
|
||||
'form_section_content' => 'İçerik',
|
||||
'form_section_page_settings' => 'Sayfa Ayarları',
|
||||
'form_section_seo_settings' => 'SEO Ayarları',
|
||||
'form_section_sections' => 'Sayfa Bölümleri',
|
||||
'translations_section' => 'Çeviriler',
|
||||
|
||||
// Form fields
|
||||
@@ -95,4 +96,53 @@ return [
|
||||
'slug_required' => 'URL yolu alanı zorunludur.',
|
||||
'slug_unique' => 'Bu URL yolu zaten kullanılıyor.',
|
||||
'content_required' => 'İçerik alanı zorunludur.',
|
||||
|
||||
// Section Builder
|
||||
'sections_field' => 'Sayfa Bölümleri',
|
||||
'sections_helper_text' => 'Sayfanız için dinamik bölümler ekleyin',
|
||||
'section_type' => 'Bölüm Tipi',
|
||||
'section_type_helper' => 'Eklemek istediğiniz bölüm tipini seçin',
|
||||
'section_data' => 'Bölüm Verileri',
|
||||
'section_data_helper' => 'Bu bölüm için gerekli verileri girin',
|
||||
'section_add' => 'Yeni Bölüm Ekle',
|
||||
|
||||
// Section Types
|
||||
'section_type_hero' => 'Hero (Ana Banner)',
|
||||
'section_type_features' => 'Özellikler',
|
||||
'section_type_stats' => 'İstatistikler',
|
||||
'section_type_cta' => 'Harekete Geçirme',
|
||||
'section_type_content' => 'İçerik',
|
||||
'section_type_gallery' => 'Galeri',
|
||||
'section_type_testimonials' => 'Referanslar',
|
||||
'section_type_team' => 'Ekip',
|
||||
'section_type_pricing' => 'Fiyatlandırma',
|
||||
'section_type_faq' => 'SSS',
|
||||
'section_type_contact' => 'İletişim',
|
||||
'section_type_custom' => 'Özel',
|
||||
|
||||
// Section Data Fields
|
||||
'section_data_key' => 'Anahtar',
|
||||
'section_data_key_helper' => 'Veri anahtarı (örn: title, subtitle, image)',
|
||||
'section_data_value_type' => 'Değer Tipi',
|
||||
'section_data_value' => 'Değer',
|
||||
'section_data_add_field' => 'Yeni Alan Ekle',
|
||||
|
||||
// Value Types
|
||||
'value_type_text' => 'Metin',
|
||||
'value_type_textarea' => 'Uzun Metin',
|
||||
'value_type_html' => 'HTML',
|
||||
'value_type_markdown' => 'Markdown',
|
||||
'value_type_richtext' => 'Zengin Metin',
|
||||
'value_type_image' => 'Görsel',
|
||||
'value_type_file' => 'Dosya',
|
||||
'value_type_url' => 'URL',
|
||||
'value_type_email' => 'E-posta',
|
||||
'value_type_phone' => 'Telefon',
|
||||
'value_type_number' => 'Sayı',
|
||||
'value_type_boolean' => 'Evet/Hayır',
|
||||
'value_type_color' => 'Renk',
|
||||
'value_type_date' => 'Tarih',
|
||||
'value_type_datetime' => 'Tarih & Saat',
|
||||
'value_type_array' => 'Dizi',
|
||||
'value_type_json' => 'JSON',
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user