diff --git a/.cursorrules b/.cursorrules
index 871db0a..0ad41be 100644
--- a/.cursorrules
+++ b/.cursorrules
@@ -2,6 +2,14 @@
## Proje Genel Kuralları
+### UI Kuralları
+- Filament 4.x'te UI kurallarını kullan
+- Filament 4.x'te yeni komponentleri kullan
+- Filament 4.x'te yeni özellikleri kullan
+- Filament 4.x'te yeni modüller oluştur
+- Filament 4.x'te yeni modüller oluştur
+- Filament UI bileşenlerini kullan x-filament:: ile olan
+
### Dil ve Lokalizasyon
- Tüm metinler Laravel Localization kullanmalı
- Türkçe ve İngilizce dil desteği zorunlu
@@ -85,6 +93,10 @@ $table
DeleteBulkAction::make(),
]),
]);
+
+// Utilities (Schema içinde kullanım)
+use Filament\Schemas\Components\Utilities\Get; // ✅ Doğru - Form state okuma
+use Filament\Schemas\Components\Utilities\Set; // ✅ Doğru - Form state yazma
```
#### **Form & Schema Components (DOĞRU):**
@@ -189,8 +201,11 @@ public static function form(Schema $schema): Schema
use Filament\Tables\Actions\EditAction; // ❌
use Filament\Forms\Components\Section; // ❌
use Filament\Forms\Components\Grid; // ❌
+use Filament\Forms\Components\Group; // ❌ Forms namespace altında Group YOK
use Filament\Forms\Components\Tabs; // ❌ Forms namespace altında Tabs YOK
use Filament\Resources\Components\Tab; // ❌ Bu class hiç YOK
+use Filament\Forms\Get; // ❌ Bu namespace YOK
+use Filament\Forms\Set; // ❌ Bu namespace YOK
// ❌ YANLIŞ - Eski metod adları
$table
@@ -204,11 +219,163 @@ $table
- ✅ **Table Filters:** `Filament\Tables\Filters\` namespace
- ✅ **Form Inputs:** `Filament\Forms\Components\` namespace
- ✅ **Layout Components:** `Filament\Schemas\Components\` namespace
+ - Section, Grid, Fieldset, Flex, Group, Tabs, View vb.
- ✅ **Tabs (Tüm Kullanımlar):** `Filament\Schemas\Components\Tabs\Tab` (Her yerde bu!)
+- ✅ **Utilities:** `Filament\Schemas\Components\Utilities\` namespace
+ - Get, Set (Schema içinde form state okuma/yazma)
- ✅ **Infolist Entries:** `Filament\Infolists\Components\` namespace
- ✅ **Table Methods:** `->recordActions()` ve `->toolbarActions()`
-**Ö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!
+**Kritik Notlar:**
+- `Filament\Resources\Components\Tab` diye bir class **YOK**. Tüm tab kullanımları `Filament\Schemas\Components\Tabs\Tab` ile yapılır!
+- `Filament\Forms\Components\Group` diye bir class **YOK**. Schema içinde gruplamak için `Filament\Schemas\Components\Group` kullan!
+- `Filament\Forms\Get` ve `Filament\Forms\Set` diye class'lar **YOK**. `Filament\Schemas\Components\Utilities\Get` ve `Filament\Schemas\Components\Utilities\Set` kullan!
+
+#### **Filament 4.x VIEW COMPONENT KULLANIM KURALLARI (KRİTİK!):**
+
+View component'ine veri iletmek için **sadece `viewData()` metodu** kullanılır:
+
+```php
+use Filament\Schemas\Components\View;
+
+// ✅ DOĞRU - viewData() ile veri gönder
+View::make('components.template-preview')
+ ->viewData([
+ 'type' => 'header',
+ 'fieldName' => 'html_content',
+ ])
+ ->columnSpanFull();
+
+// ❌ YANLIŞ - with() metodu ÇALIŞMAZ!
+View::make('components.template-preview')
+ ->with([...]) // ❌ Bu metod View component'inde YOK!
+
+// ❌ YANLIŞ - extraAttributes() sadece HTML attribute'leri için
+View::make('components.template-preview')
+ ->extraAttributes([
+ 'data-type' => 'header', // ❌ Bu view'e değişken olarak GİTMEZ!
+ ])
+```
+
+**Özet:**
+- ✅ `->viewData([...])` → Blade view'e değişken gönderir
+- ❌ `->with([...])` → View component'inde bu metod YOK
+- ❌ `->extraAttributes([...])` → Sadece HTML attribute'leri için, view değişkenleri için DEĞİL
+
+#### **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ı
getFirstGuide();
+
+ if ($firstGuide) {
+ $this->selectedGuide = $firstGuide['slug'];
+ }
+ }
+
+ public static function getNavigationLabel(): string
+ {
+ return __('user-guide.navigation_label');
+ }
+
+ public function getTitle(): string
+ {
+ return __('user-guide.title');
+ }
+
+ public function getMaxContentWidth(): Width | string | null
+ {
+ return Width::Full;
+ }
+
+ public function getGuides(): array
+ {
+ $guideService = app(GuideService::class);
+ return $guideService->getAllGuides();
+ }
+
+ public function getSelectedGuideContent(): ?array
+ {
+ if (!$this->selectedGuide) {
+ return null;
+ }
+
+ $guideService = app(GuideService::class);
+ return $guideService->getGuide($this->selectedGuide);
+ }
+
+ public function selectGuide(string $slug): void
+ {
+ $guideService = app(GuideService::class);
+
+ if ($guideService->guideExists($slug)) {
+ $this->selectedGuide = $slug;
+ }
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php
index 495d912..82005d3 100644
--- a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php
+++ b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php
@@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\Blogs\Pages;
use App\Filament\Admin\Resources\Blogs\BlogResource;
use App\Filament\Admin\Resources\Components\TranslationTabs;
+use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
@@ -21,14 +22,33 @@ class EditBlog extends EditRecord
protected function getHeaderActions(): array
{
return [
+ Action::make('save')
+ ->label(__('blog.save'))
+ ->action('save')
+ ->keyBindings(['mod+s'])
+ ->color('primary')
+ ->size('sm'),
+ Action::make('cancel')
+ ->label(__('blog.cancel'))
+ ->url($this->getResource()::getUrl('index'))
+ ->color('gray')
+ ->size('sm'),
DeleteAction::make()
- ->label(__('blog.delete')),
+ ->label(__('blog.delete'))
+ ->size('sm'),
RestoreAction::make()
- ->label(__('blog.restore')),
+ ->label(__('blog.restore'))
+ ->size('sm'),
ForceDeleteAction::make()
- ->label(__('blog.force_delete')),
+ ->label(__('blog.force_delete'))
+ ->size('sm'),
];
}
+
+ protected function getFormActions(): array
+ {
+ return []; // Boş array döndürerek alt taraftaki form action'larını gizliyoruz
+ }
protected function getRedirectUrl(): string
{
diff --git a/app/Filament/Admin/Resources/Components/TranslationTabs.php b/app/Filament/Admin/Resources/Components/TranslationTabs.php
index b0c61c9..b88144f 100644
--- a/app/Filament/Admin/Resources/Components/TranslationTabs.php
+++ b/app/Filament/Admin/Resources/Components/TranslationTabs.php
@@ -167,10 +167,20 @@ class TranslationTabs
$languages = Language::active()->ordered()->get();
$fields = $record->getTranslatableFields();
+ // Eager load all translations to avoid N+1 queries
+ $allTranslations = $record->translations()
+ ->whereIn('language_code', $languages->pluck('code'))
+ ->whereIn('field_name', $fields)
+ ->get()
+ ->groupBy('language_code');
+
foreach ($languages as $language) {
$data['translations'][$language->code] = [];
+ $languageTranslations = $allTranslations->get($language->code) ?? collect();
+ $translationsByField = $languageTranslations->keyBy('field_name');
+
foreach ($fields as $field) {
- $translation = $record->getTranslation($field, $language->code, false);
+ $translation = $translationsByField->get($field);
if ($translation) {
$data['translations'][$language->code][$field] = $translation->field_value;
}
diff --git a/app/Filament/Admin/Resources/Customers/Pages/EditCustomer.php b/app/Filament/Admin/Resources/Customers/Pages/EditCustomer.php
index 7a0b5e5..9287cc1 100644
--- a/app/Filament/Admin/Resources/Customers/Pages/EditCustomer.php
+++ b/app/Filament/Admin/Resources/Customers/Pages/EditCustomer.php
@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Customers\Pages;
use App\Filament\Admin\Resources\Customers\CustomerResource;
+use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
@@ -13,7 +14,24 @@ class EditCustomer extends EditRecord
protected function getHeaderActions(): array
{
return [
- DeleteAction::make(),
+ Action::make('save')
+ ->label('Save')
+ ->action('save')
+ ->keyBindings(['mod+s'])
+ ->color('primary')
+ ->size('sm'),
+ Action::make('cancel')
+ ->label('Cancel')
+ ->url($this->getResource()::getUrl('index'))
+ ->color('gray')
+ ->size('sm'),
+ DeleteAction::make()
+ ->size('sm'),
];
}
+
+ protected function getFormActions(): array
+ {
+ return []; // Boş array döndürerek alt taraftaki form action'larını gizliyoruz
+ }
}
diff --git a/app/Filament/Admin/Resources/FooterTemplates/FooterTemplateResource.php b/app/Filament/Admin/Resources/FooterTemplates/FooterTemplateResource.php
new file mode 100644
index 0000000..164bfb5
--- /dev/null
+++ b/app/Filament/Admin/Resources/FooterTemplates/FooterTemplateResource.php
@@ -0,0 +1,86 @@
+ ListFooterTemplates::route('/'),
+ 'create' => CreateFooterTemplate::route('/create'),
+ 'view' => ViewFooterTemplate::route('/{record}'),
+ 'edit' => EditFooterTemplate::route('/{record}/edit'),
+ ];
+ }
+
+ public static function getRecordRouteBindingEloquentQuery(): Builder
+ {
+ return parent::getRecordRouteBindingEloquentQuery()
+ ->withoutGlobalScopes([
+ SoftDeletingScope::class,
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/FooterTemplates/Pages/CreateFooterTemplate.php b/app/Filament/Admin/Resources/FooterTemplates/Pages/CreateFooterTemplate.php
new file mode 100644
index 0000000..6d0d8f4
--- /dev/null
+++ b/app/Filament/Admin/Resources/FooterTemplates/Pages/CreateFooterTemplate.php
@@ -0,0 +1,41 @@
+ $value) {
+ if (str_starts_with($key, 'default_data.')) {
+ // Remove 'default_data.' prefix to get the nested key
+ $nestedKey = substr($key, 13); // 'default_data.' length is 13
+ $defaultData[$nestedKey] = $value;
+ // Remove the flattened key from data
+ unset($data[$key]);
+ }
+ }
+
+ // Set the properly formatted default_data array
+ if (!empty($defaultData)) {
+ $data['default_data'] = $defaultData;
+ }
+
+ return $data;
+ }
+}
diff --git a/app/Filament/Admin/Resources/FooterTemplates/Pages/EditFooterTemplate.php b/app/Filament/Admin/Resources/FooterTemplates/Pages/EditFooterTemplate.php
new file mode 100644
index 0000000..bdc8049
--- /dev/null
+++ b/app/Filament/Admin/Resources/FooterTemplates/Pages/EditFooterTemplate.php
@@ -0,0 +1,62 @@
+ $value) {
+ if (str_starts_with($key, 'default_data.')) {
+ // Remove 'default_data.' prefix to get the nested key
+ $nestedKey = substr($key, 13); // 'default_data.' length is 13
+ $defaultData[$nestedKey] = $value;
+ // Remove the flattened key from data
+ unset($data[$key]);
+ }
+ }
+
+ // Set the properly formatted default_data array
+ if (!empty($defaultData)) {
+ $data['default_data'] = $defaultData;
+ }
+
+ return $data;
+ }
+
+ protected function afterSave(): void
+ {
+ // Kaydetme sonrası preview'ı güncelle
+ // Livewire event dispatch et
+ $this->dispatch('template-saved');
+ }
+}
diff --git a/app/Filament/Admin/Resources/FooterTemplates/Pages/ListFooterTemplates.php b/app/Filament/Admin/Resources/FooterTemplates/Pages/ListFooterTemplates.php
new file mode 100644
index 0000000..61c83ea
--- /dev/null
+++ b/app/Filament/Admin/Resources/FooterTemplates/Pages/ListFooterTemplates.php
@@ -0,0 +1,19 @@
+components([
+ Section::make(__('footer-templates.section_general'))
+ ->schema([
+ TextInput::make('title')
+ ->label(__('footer-templates.field_title'))
+ ->required()
+ ->maxLength(255)
+ ->columnSpanFull(),
+
+ // Placeholder Picker Component
+ View::make('components.placeholder-picker')
+ ->viewData([
+ 'fieldName' => 'html_content',
+ ])
+ ->columnSpanFull(),
+
+ CodeEditor::make('html_content')
+ ->label(__('footer-templates.field_html_content'))
+ ->required()
+ ->language(Language::Html)
+ ->live(onBlur: false) // Real-time updates
+ ->columnSpanFull()
+ ->helperText(__('footer-templates.field_html_content_help'))
+ ->extraAttributes([
+ 'style' => 'max-height: 400px;overflow-y: auto;',
+ 'data-field-name' => 'html_content',
+ ]),
+
+ // Preview Component - placed after editor
+ View::make('components.template-preview')
+ ->viewData([
+ 'type' => 'footer',
+ 'fieldName' => 'html_content',
+ 'recordId' => null, // Will be extracted from URL by JavaScript
+ ])
+ ->columnSpanFull(),
+
+ Toggle::make('is_active')
+ ->label(__('footer-templates.field_is_active'))
+ ->default(true)
+ ->inline(false),
+ ])
+ ->columnSpanFull(),
+
+ // Default Data Section - Dynamic fields based on placeholders
+ Section::make(__('footer-templates.section_default_data'))
+ ->description(__('footer-templates.section_default_data_desc'))
+ ->schema([
+ Group::make()
+ ->schema(function (Get $get, $record): array {
+ $htmlContent = $get('html_content');
+ if (empty($htmlContent)) {
+ return [];
+ }
+
+ // Create a temporary template object to parse placeholders
+ $tempTemplate = new FooterTemplate();
+ $tempTemplate->html_content = $htmlContent;
+
+ // Get existing default_data if editing
+ $existingData = $record ? ($record->default_data ?? []) : [];
+
+ return TemplateService::generateDynamicFields(
+ $tempTemplate,
+ 'default_data',
+ $existingData,
+ null // No fallback needed for template's own default_data
+ );
+ })
+ ->key('default_data_fields')
+ ->visible(fn (Get $get): bool => filled($get('html_content')))
+ ->columnSpanFull(),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(false),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateInfolist.php b/app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateInfolist.php
new file mode 100644
index 0000000..05f6022
--- /dev/null
+++ b/app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateInfolist.php
@@ -0,0 +1,16 @@
+components([
+ //
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/FooterTemplates/Tables/FooterTemplatesTable.php b/app/Filament/Admin/Resources/FooterTemplates/Tables/FooterTemplatesTable.php
new file mode 100644
index 0000000..f7c68fc
--- /dev/null
+++ b/app/Filament/Admin/Resources/FooterTemplates/Tables/FooterTemplatesTable.php
@@ -0,0 +1,70 @@
+columns([
+ TextColumn::make('title')
+ ->label(__('footer-templates.column_title'))
+ ->searchable()
+ ->sortable(),
+
+ IconColumn::make('is_active')
+ ->label(__('footer-templates.column_is_active'))
+ ->boolean()
+ ->sortable(),
+
+ TextColumn::make('pages_count')
+ ->label(__('footer-templates.column_pages_count'))
+ ->counts('pages')
+ ->sortable(),
+
+ TextColumn::make('created_at')
+ ->label(__('footer-templates.column_created_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('updated_at')
+ ->label(__('footer-templates.column_updated_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('deleted_at')
+ ->label(__('footer-templates.column_deleted_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ ])
+ ->filters([
+ TrashedFilter::make(),
+ ])
+ ->recordActions([
+ ViewAction::make(),
+ EditAction::make(),
+ ])
+ ->toolbarActions([
+ BulkActionGroup::make([
+ DeleteBulkAction::make(),
+ ForceDeleteBulkAction::make(),
+ RestoreBulkAction::make(),
+ ]),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/HeaderTemplates/HeaderTemplateResource.php b/app/Filament/Admin/Resources/HeaderTemplates/HeaderTemplateResource.php
new file mode 100644
index 0000000..22918c4
--- /dev/null
+++ b/app/Filament/Admin/Resources/HeaderTemplates/HeaderTemplateResource.php
@@ -0,0 +1,86 @@
+ ListHeaderTemplates::route('/'),
+ 'create' => CreateHeaderTemplate::route('/create'),
+ 'view' => ViewHeaderTemplate::route('/{record}'),
+ 'edit' => EditHeaderTemplate::route('/{record}/edit'),
+ ];
+ }
+
+ public static function getRecordRouteBindingEloquentQuery(): Builder
+ {
+ return parent::getRecordRouteBindingEloquentQuery()
+ ->withoutGlobalScopes([
+ SoftDeletingScope::class,
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Pages/CreateHeaderTemplate.php b/app/Filament/Admin/Resources/HeaderTemplates/Pages/CreateHeaderTemplate.php
new file mode 100644
index 0000000..77a7ad4
--- /dev/null
+++ b/app/Filament/Admin/Resources/HeaderTemplates/Pages/CreateHeaderTemplate.php
@@ -0,0 +1,41 @@
+ $value) {
+ if (str_starts_with($key, 'default_data.')) {
+ // Remove 'default_data.' prefix to get the nested key
+ $nestedKey = substr($key, 13); // 'default_data.' length is 13
+ $defaultData[$nestedKey] = $value;
+ // Remove the flattened key from data
+ unset($data[$key]);
+ }
+ }
+
+ // Set the properly formatted default_data array
+ if (!empty($defaultData)) {
+ $data['default_data'] = $defaultData;
+ }
+
+ return $data;
+ }
+}
diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Pages/EditHeaderTemplate.php b/app/Filament/Admin/Resources/HeaderTemplates/Pages/EditHeaderTemplate.php
new file mode 100644
index 0000000..fa6b6d5
--- /dev/null
+++ b/app/Filament/Admin/Resources/HeaderTemplates/Pages/EditHeaderTemplate.php
@@ -0,0 +1,62 @@
+ $value) {
+ if (str_starts_with($key, 'default_data.')) {
+ // Remove 'default_data.' prefix to get the nested key
+ $nestedKey = substr($key, 13); // 'default_data.' length is 13
+ $defaultData[$nestedKey] = $value;
+ // Remove the flattened key from data
+ unset($data[$key]);
+ }
+ }
+
+ // Set the properly formatted default_data array
+ if (!empty($defaultData)) {
+ $data['default_data'] = $defaultData;
+ }
+
+ return $data;
+ }
+
+ protected function afterSave(): void
+ {
+ // Kaydetme sonrası preview'ı güncelle
+ // Livewire event dispatch et (JavaScript tarafında Livewire.on() ile dinlenecek)
+ $this->dispatch('template-saved');
+ }
+}
diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Pages/ListHeaderTemplates.php b/app/Filament/Admin/Resources/HeaderTemplates/Pages/ListHeaderTemplates.php
new file mode 100644
index 0000000..b001b41
--- /dev/null
+++ b/app/Filament/Admin/Resources/HeaderTemplates/Pages/ListHeaderTemplates.php
@@ -0,0 +1,19 @@
+components([
+ Section::make(__('header-templates.section_general'))
+ ->schema([
+ TextInput::make('title')
+ ->label(__('header-templates.field_title'))
+ ->required()
+ ->maxLength(255)
+ ->columnSpanFull(),
+
+ // Placeholder Picker Component
+ View::make('components.placeholder-picker')
+ ->viewData([
+ 'fieldName' => 'html_content',
+ ])
+ ->columnSpanFull(),
+
+ CodeEditor::make('html_content')
+ ->label(__('header-templates.field_html_content'))
+ ->required()
+ ->live(onBlur: true) // Real-time updates
+ ->language(Language::Html)
+ ->columnSpanFull()
+ ->helperText(__('header-templates.field_html_content_help'))
+ ->extraAttributes([
+ 'style' => 'max-height: 400px;overflow-y: auto;',
+ 'data-field-name' => 'html_content',
+ ]),
+
+ // Preview Component - placed after editor
+ View::make('components.template-preview')
+ ->viewData([
+ 'type' => 'header',
+ 'fieldName' => 'html_content',
+ 'recordId' => null, // Will be extracted from URL by JavaScript
+ ])
+ ->columnSpanFull(),
+
+ Toggle::make('is_active')
+ ->label(__('header-templates.field_is_active'))
+ ->default(true)
+ ->inline(false),
+ ])
+ ->columnSpanFull(),
+
+ // Default Data Section - Dynamic fields based on placeholders
+ Section::make(__('header-templates.section_default_data'))
+ ->description(__('header-templates.section_default_data_desc'))
+ ->schema([
+ Group::make()
+ ->schema(function (Get $get, $record): array {
+ $htmlContent = $get('html_content');
+ if (empty($htmlContent)) {
+ return [];
+ }
+
+ // Create a temporary template object to parse placeholders
+ $tempTemplate = new HeaderTemplate();
+ $tempTemplate->html_content = $htmlContent;
+
+ // Get existing default_data if editing
+ $existingData = $record ? ($record->default_data ?? []) : [];
+
+ return TemplateService::generateDynamicFields(
+ $tempTemplate,
+ 'default_data',
+ $existingData,
+ null // No fallback needed for template's own default_data
+ );
+ })
+ ->key('default_data_fields')
+ ->visible(fn (Get $get): bool => filled($get('html_content')))
+ ->columnSpanFull(),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(false),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateInfolist.php b/app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateInfolist.php
new file mode 100644
index 0000000..c6334a4
--- /dev/null
+++ b/app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateInfolist.php
@@ -0,0 +1,16 @@
+components([
+ //
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Tables/HeaderTemplatesTable.php b/app/Filament/Admin/Resources/HeaderTemplates/Tables/HeaderTemplatesTable.php
new file mode 100644
index 0000000..0ae9b2f
--- /dev/null
+++ b/app/Filament/Admin/Resources/HeaderTemplates/Tables/HeaderTemplatesTable.php
@@ -0,0 +1,70 @@
+columns([
+ TextColumn::make('title')
+ ->label(__('header-templates.column_title'))
+ ->searchable()
+ ->sortable(),
+
+ IconColumn::make('is_active')
+ ->label(__('header-templates.column_is_active'))
+ ->boolean()
+ ->sortable(),
+
+ TextColumn::make('pages_count')
+ ->label(__('header-templates.column_pages_count'))
+ ->counts('pages')
+ ->sortable(),
+
+ TextColumn::make('created_at')
+ ->label(__('header-templates.column_created_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('updated_at')
+ ->label(__('header-templates.column_updated_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('deleted_at')
+ ->label(__('header-templates.column_deleted_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ ])
+ ->filters([
+ TrashedFilter::make(),
+ ])
+ ->recordActions([
+ ViewAction::make(),
+ EditAction::make(),
+ ])
+ ->toolbarActions([
+ BulkActionGroup::make([
+ DeleteBulkAction::make(),
+ ForceDeleteBulkAction::make(),
+ RestoreBulkAction::make(),
+ ]),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php b/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php
index 91a6a1a..d837fe3 100644
--- a/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php
+++ b/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php
@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Languages\Pages;
use App\Filament\Admin\Resources\Languages\LanguageResource;
+use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
@@ -21,14 +22,33 @@ class EditLanguage extends EditRecord
protected function getHeaderActions(): array
{
return [
+ Action::make('save')
+ ->label(__('language.save'))
+ ->action('save')
+ ->keyBindings(['mod+s'])
+ ->color('primary')
+ ->size('sm'),
+ Action::make('cancel')
+ ->label(__('language.cancel'))
+ ->url($this->getResource()::getUrl('index'))
+ ->color('gray')
+ ->size('sm'),
DeleteAction::make()
- ->label(__('language.delete')),
+ ->label(__('language.delete'))
+ ->size('sm'),
RestoreAction::make()
- ->label(__('language.restore')),
+ ->label(__('language.restore'))
+ ->size('sm'),
ForceDeleteAction::make()
- ->label(__('language.force_delete')),
+ ->label(__('language.force_delete'))
+ ->size('sm'),
];
}
+
+ protected function getFormActions(): array
+ {
+ return []; // Boş array döndürerek alt taraftaki form action'larını gizliyoruz
+ }
protected function getSavedNotification(): ?Notification
{
diff --git a/app/Filament/Admin/Resources/MenuTemplates/MenuTemplateResource.php b/app/Filament/Admin/Resources/MenuTemplates/MenuTemplateResource.php
new file mode 100644
index 0000000..58196a8
--- /dev/null
+++ b/app/Filament/Admin/Resources/MenuTemplates/MenuTemplateResource.php
@@ -0,0 +1,86 @@
+ ListMenuTemplates::route('/'),
+ 'create' => CreateMenuTemplate::route('/create'),
+ 'view' => ViewMenuTemplate::route('/{record}'),
+ 'edit' => EditMenuTemplate::route('/{record}/edit'),
+ ];
+ }
+
+ public static function getRecordRouteBindingEloquentQuery(): Builder
+ {
+ return parent::getRecordRouteBindingEloquentQuery()
+ ->withoutGlobalScopes([
+ SoftDeletingScope::class,
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/MenuTemplates/Pages/CreateMenuTemplate.php b/app/Filament/Admin/Resources/MenuTemplates/Pages/CreateMenuTemplate.php
new file mode 100644
index 0000000..eb32877
--- /dev/null
+++ b/app/Filament/Admin/Resources/MenuTemplates/Pages/CreateMenuTemplate.php
@@ -0,0 +1,18 @@
+dispatch('template-saved');
+ }
+}
diff --git a/app/Filament/Admin/Resources/MenuTemplates/Pages/ListMenuTemplates.php b/app/Filament/Admin/Resources/MenuTemplates/Pages/ListMenuTemplates.php
new file mode 100644
index 0000000..6528c64
--- /dev/null
+++ b/app/Filament/Admin/Resources/MenuTemplates/Pages/ListMenuTemplates.php
@@ -0,0 +1,19 @@
+components([
+ Section::make(__('menu-templates.section_general'))
+ ->schema([
+ TextInput::make('title')
+ ->label(__('menu-templates.field_title'))
+ ->required()
+ ->maxLength(255)
+ ->columnSpanFull(),
+
+ // Placeholder Picker Component
+ View::make('components.placeholder-picker')
+ ->viewData([
+ 'fieldName' => 'html_content',
+ ])
+ ->columnSpanFull(),
+
+ CodeEditor::make('html_content')
+ ->label(__('menu-templates.field_html_content'))
+ ->required()
+ ->live(onBlur: true)
+ ->language(Language::Html)
+ ->columnSpanFull()
+ ->helperText(__('menu-templates.field_html_content_help'))
+ ->extraAttributes([
+ 'style' => 'max-height: 400px;overflow-y: auto;',
+ 'data-field-name' => 'html_content',
+ ]),
+
+ // Preview Component
+ View::make('components.menu-template-preview')
+ ->viewData([
+ 'type' => 'menu',
+ 'fieldName' => 'html_content',
+ 'recordId' => null, // Will be extracted from URL by JavaScript
+ ])
+ ->columnSpanFull(),
+
+ Toggle::make('is_active')
+ ->label(__('menu-templates.field_is_active'))
+ ->default(true)
+ ->inline(false),
+ ])
+ ->columnSpanFull(),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/MenuTemplates/Schemas/MenuTemplateInfolist.php b/app/Filament/Admin/Resources/MenuTemplates/Schemas/MenuTemplateInfolist.php
new file mode 100644
index 0000000..7057bf7
--- /dev/null
+++ b/app/Filament/Admin/Resources/MenuTemplates/Schemas/MenuTemplateInfolist.php
@@ -0,0 +1,17 @@
+components([
+ //
+ ]);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/MenuTemplates/Tables/MenuTemplatesTable.php b/app/Filament/Admin/Resources/MenuTemplates/Tables/MenuTemplatesTable.php
new file mode 100644
index 0000000..67f903c
--- /dev/null
+++ b/app/Filament/Admin/Resources/MenuTemplates/Tables/MenuTemplatesTable.php
@@ -0,0 +1,65 @@
+columns([
+ TextColumn::make('title')
+ ->label(__('menu-templates.column_title'))
+ ->searchable()
+ ->sortable(),
+
+ IconColumn::make('is_active')
+ ->label(__('menu-templates.column_is_active'))
+ ->boolean()
+ ->sortable(),
+
+ TextColumn::make('created_at')
+ ->label(__('menu-templates.column_created_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('updated_at')
+ ->label(__('menu-templates.column_updated_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('deleted_at')
+ ->label(__('menu-templates.column_deleted_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ ])
+ ->filters([
+ TrashedFilter::make(),
+ ])
+ ->recordActions([
+ ViewAction::make(),
+ EditAction::make(),
+ ])
+ ->toolbarActions([
+ BulkActionGroup::make([
+ DeleteBulkAction::make(),
+ ForceDeleteBulkAction::make(),
+ RestoreBulkAction::make(),
+ ]),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/Pages/PageResource.php b/app/Filament/Admin/Resources/Pages/PageResource.php
index d0a6d70..5eee605 100644
--- a/app/Filament/Admin/Resources/Pages/PageResource.php
+++ b/app/Filament/Admin/Resources/Pages/PageResource.php
@@ -5,6 +5,7 @@ namespace App\Filament\Admin\Resources\Pages;
use App\Filament\Admin\Resources\Pages\Pages\CreatePage;
use App\Filament\Admin\Resources\Pages\Pages\EditPage;
use App\Filament\Admin\Resources\Pages\Pages\ListPages;
+use App\Filament\Admin\Resources\Pages\Pages\MenuTree;
use App\Filament\Admin\Resources\Pages\Schemas\PageForm;
use App\Filament\Admin\Resources\Pages\Tables\PagesTable;
use App\Models\Page;
@@ -60,6 +61,7 @@ class PageResource extends Resource
'index' => ListPages::route('/'),
'create' => CreatePage::route('/create'),
'edit' => EditPage::route('/{record}/edit'),
+ 'menu' => MenuTree::route('/menu'),
];
}
diff --git a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php
index 569c930..b820ee8 100644
--- a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php
+++ b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php
@@ -4,35 +4,89 @@ namespace App\Filament\Admin\Resources\Pages\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Pages\PageResource;
+use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
+use Illuminate\Contracts\View\View;
class EditPage extends EditRecord
{
protected static string $resource = PageResource::class;
+
+ // Sticky header aktif
+ protected static bool $hasTopbar = true;
+
+ public bool $autoSaveEnabled = false;
+ public ?string $lastAutoSaveTime = null;
+
+ public function mount($record): void
+ {
+ parent::mount($record);
+
+ // Eager load translations to avoid N+1 queries
+ $this->record->load('translations');
+ }
public function getTitle(): string
{
return __('pages.edit');
}
+
+ public function autoSave(): void
+ {
+ try {
+ $this->save();
+ $this->lastAutoSaveTime = now()->format('H:i:s');
+
+ // Sessiz başarı bildirimi
+ \Filament\Notifications\Notification::make()
+ ->title('Otomatik Kaydedildi')
+ ->body('Son kayıt: ' . $this->lastAutoSaveTime)
+ ->success()
+ ->duration(2000)
+ ->send();
+ } catch (\Exception $e) {
+ // Hata olursa sessizce geç, kullanıcıyı rahatsız etme
+ }
+ }
protected function getHeaderActions(): array
{
return [
+
+ Action::make('save')
+ ->label(__('pages.save'))
+ ->action('save')
+ ->keyBindings(['mod+s'])
+ ->color('primary')
+ ->size('sm'),
+ Action::make('cancel')
+ ->label(__('pages.cancel'))
+ ->url($this->getResource()::getUrl('index'))
+ ->color('gray')
+ ->size('sm'),
DeleteAction::make()
- ->label(__('pages.delete')),
+ ->label(__('pages.delete'))
+ ->size('sm'),
ForceDeleteAction::make()
- ->label(__('pages.force_delete')),
+ ->label(__('pages.force_delete'))
+ ->size('sm'),
RestoreAction::make()
- ->label(__('pages.restore')),
+ ->label(__('pages.restore'))
+ ->size('sm'),
];
}
-
- protected function getRedirectUrl(): string
+
+ protected function getFormActions(): array
{
- return $this->getResource()::getUrl('index');
+ return []; // Boş array döndürerek alt taraftaki form action'larını gizliyoruz
+ }
+
+ protected function getRedirectUrl(): ?string
+ {
+ return null; // Aynı sayfada kal
}
protected function getSavedNotificationTitle(): ?string
@@ -42,9 +96,13 @@ class EditPage extends EditRecord
protected function mutateFormDataBeforeFill(array $data): array
{
- // Load existing translations
+ // Load existing translations with eager loading
$translationData = TranslationTabs::fillFromRecord($this->record);
+ // DISABLED: Old format conversion causes memory exhaustion with nested arrays
+ // Leave sections as-is; they work fine in their original format
+ // If needed, conversion can be done manually or in a separate migration
+
return array_merge($data, $translationData);
}
@@ -53,4 +111,11 @@ class EditPage extends EditRecord
// Save translations
TranslationTabs::saveTranslations($this->record, $this->form->getState());
}
+
+ public function getFooter(): ?View
+ {
+ return view('filament.pages.edit-page-footer', [
+ 'autoSaveEnabled' => $this->autoSaveEnabled,
+ ]);
+ }
}
diff --git a/app/Filament/Admin/Resources/Pages/Pages/ListPages.php b/app/Filament/Admin/Resources/Pages/Pages/ListPages.php
index 3a1f431..a9f4e26 100644
--- a/app/Filament/Admin/Resources/Pages/Pages/ListPages.php
+++ b/app/Filament/Admin/Resources/Pages/Pages/ListPages.php
@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Pages\Pages;
use App\Filament\Admin\Resources\Pages\PageResource;
+use Filament\Actions\Action;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
@@ -18,6 +19,11 @@ class ListPages extends ListRecords
protected function getHeaderActions(): array
{
return [
+ Action::make('menu')
+ ->label(__('pages.menu_tree_title'))
+ ->icon('heroicon-o-bars-3')
+ ->url(PageResource::getUrl('menu'))
+ ->color('gray'),
CreateAction::make()
->label(__('pages.create')),
];
diff --git a/app/Filament/Admin/Resources/Pages/Pages/MenuTree.php b/app/Filament/Admin/Resources/Pages/Pages/MenuTree.php
new file mode 100644
index 0000000..db38ffe
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Pages/MenuTree.php
@@ -0,0 +1,44 @@
+label(__('pages.menu_refresh'))
+ ->icon('heroicon-o-arrow-path')
+ ->color('gray')
+ ->action(function () {
+ $this->dispatch('$refresh');
+ }),
+ Action::make('back_to_pages')
+ ->label(__('pages.menu_back_to_pages'))
+ ->icon('heroicon-o-arrow-left')
+ ->url(PageResource::getUrl('index'))
+ ->color('gray'),
+ ];
+ }
+
+ protected function getFooterWidgets(): array
+ {
+ return [
+ PagesMenuWidget::class,
+ ];
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php
index ceebbff..2053f3f 100644
--- a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php
+++ b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php
@@ -1,209 +1,56 @@
columns(3)
->schema([
- // Sol kolon - Ana içerik (2 sütun genişliğinde)
- Section::make(__('pages.form_section_content'))
- ->schema([
- TextInput::make('title')
- ->label(__('pages.title_field'))
- ->required()
- ->maxLength(255)
- ->live(onBlur: true)
- ->afterStateUpdated(function (string $operation, $state, callable $set) {
- if ($operation !== 'create') {
- return;
- }
- $set('slug', \Str::slug($state));
- }),
-
- TextInput::make('slug')
- ->label(__('pages.slug_field'))
- ->required()
- ->maxLength(255)
- ->unique(ignoreRecord: true)
- ->rules(['alpha_dash'])
- ->helperText(__('pages.slug_helper_text')),
-
- RichEditor::make('content')
- ->label(__('pages.content_field'))
- ->required()
- ->fileAttachmentsDisk('public')
- ->fileAttachmentsDirectory('pages')
- ->fileAttachmentsVisibility('public')
- ->columnSpanFull(),
-
- Textarea::make('excerpt')
- ->label(__('pages.excerpt_field'))
- ->rows(3)
- ->maxLength(500)
- ->helperText(__('pages.excerpt_helper_text'))
- ->columnSpanFull(),
- ])
- ->columnSpan(2)
- ->collapsible(false),
+ // Left Column - Main Content (2 columns width)
+ ContentSection::make(),
- // Sağ kolon - Metadata ve öne çıkan görsel (1 sütun genişliğinde)
- Section::make(__('pages.form_section_page_settings'))
- ->schema([
- FileUpload::make('featured_image')
- ->label(__('pages.featured_image_field'))
- ->image()
- ->disk('public')
- ->directory('pages/featured')
- ->visibility('public')
- ->imageEditor()
- ->imageEditorAspectRatios([
- '16:9',
- '4:3',
- '1:1',
- ])
- ->helperText(__('pages.featured_image_helper_text'))
- ->columnSpanFull(),
-
- Select::make('author_id')
- ->label(__('pages.author_field'))
- ->relationship('author', 'name')
- ->default(auth()->id())
- ->required()
- ->columnSpanFull(),
-
- DateTimePicker::make('published_at')
- ->label(__('pages.published_at_field'))
- ->displayFormat('d.m.Y H:i')
- ->helperText(__('pages.published_at_helper_text'))
- ->columnSpanFull(),
-
- Select::make('status')
- ->label(__('pages.status_field'))
- ->options([
- 'draft' => __('pages.status_draft'),
- 'published' => __('pages.status_published'),
- 'archived' => __('pages.status_archived'),
- ])
- ->default('draft')
- ->required()
- ->columnSpanFull(),
-
- Select::make('parent_id')
- ->label(__('pages.parent_field'))
- ->relationship('parent', 'title')
- ->searchable()
- ->preload()
- ->helperText(__('pages.parent_helper_text'))
- ->columnSpanFull(),
-
- Select::make('template')
- ->label(__('pages.template_field'))
- ->options([
- 'default' => __('pages.template_default'),
- 'landing' => __('pages.template_landing'),
- 'blog' => __('pages.template_blog'),
- 'contact' => __('pages.template_contact'),
- ])
- ->default('default')
- ->columnSpanFull(),
-
- TextInput::make('sort_order')
- ->label(__('pages.sort_order_field'))
- ->numeric()
- ->default(0)
- ->helperText(__('pages.sort_order_helper_text'))
- ->columnSpanFull(),
-
- Checkbox::make('is_homepage')
- ->label(__('pages.is_homepage_field'))
- ->helperText(__('pages.is_homepage_helper_text'))
- ->columnSpanFull(),
-
- Checkbox::make('show_in_menu')
- ->label(__('pages.show_in_menu_field'))
- ->default(true)
- ->helperText(__('pages.show_in_menu_helper_text'))
- ->columnSpanFull(),
- ])
- ->columnSpan(1)
- ->collapsible(false),
+ // Right Column - Settings and Metadata (1 column width)
+ PageSettingsSection::make(),
- // Alt kısım - SEO ayarları (tam genişlik)
- Section::make(__('pages.form_section_seo_settings'))
- ->schema([
- TextInput::make('meta_title')
- ->label(__('pages.meta_title_field'))
- ->maxLength(60)
- ->helperText(__('pages.meta_title_helper_text')),
-
- Textarea::make('meta_description')
- ->label(__('pages.meta_description_field'))
- ->rows(3)
- ->maxLength(160)
- ->helperText(__('pages.meta_description_helper_text'))
- ->columnSpanFull(),
- ])
- ->columns(2)
- ->columnSpanFull()
- ->collapsible(true)
- ->collapsed(true),
+ // Full Width - SEO Settings
+ SeoSection::make(),
- // Çeviri Sekmesi
- Section::make('🌍 ' . __('pages.translations_section'))
- ->schema([
- TranslationTabs::make([
- 'title' => [
- 'type' => 'text',
- 'label' => __('pages.title_field'),
- 'required' => false,
- 'maxLength' => 255,
- ],
- 'content' => [
- 'type' => 'richtext',
- 'label' => __('pages.content_field'),
- 'required' => false,
- ],
- 'excerpt' => [
- 'type' => 'textarea',
- 'label' => __('pages.excerpt_field'),
- 'required' => false,
- 'maxLength' => 500,
- 'rows' => 3,
- ],
- 'meta_title' => [
- 'type' => 'text',
- 'label' => __('pages.meta_title_field'),
- 'required' => false,
- 'maxLength' => 60,
- ],
- 'meta_description' => [
- 'type' => 'textarea',
- 'label' => __('pages.meta_description_field'),
- 'required' => false,
- 'maxLength' => 160,
- 'rows' => 3,
- ],
- ]),
- ])
- ->columnSpanFull()
- ->collapsible(true)
- ->collapsed(false),
+ // Full Width - Section Builder (Custom Page Sections)
+ // SectionBuilderSection::make(),
+
+ // Full Width - Dynamic Template System
+ HeaderTemplateSection::make(),
+ SectionTemplatesSection::make(),
+ FooterTemplateSection::make(),
+
+ // Full Width - Translations
+ TranslationsSection::make(),
]);
}
}
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/ContentSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/ContentSection.php
new file mode 100644
index 0000000..81d0fd5
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/ContentSection.php
@@ -0,0 +1,55 @@
+schema([
+ TextInput::make('title')
+ ->label(__('pages.title_field'))
+ ->required()
+ ->maxLength(255)
+ ->live(onBlur: true)
+ ->afterStateUpdated(function (string $operation, $state, callable $set) {
+ if ($operation !== 'create') {
+ return;
+ }
+ $set('slug', \Str::slug($state));
+ }),
+
+ TextInput::make('slug')
+ ->label(__('pages.slug_field'))
+ ->required()
+ ->maxLength(255)
+ ->unique(ignoreRecord: true)
+ ->rules(['alpha_dash'])
+ ->helperText(__('pages.slug_helper_text')),
+
+ RichEditor::make('content')
+ ->label(__('pages.content_field'))
+ ->required()
+ ->fileAttachmentsDisk('public')
+ ->fileAttachmentsDirectory('pages')
+ ->fileAttachmentsVisibility('public')
+ ->columnSpanFull(),
+
+ Textarea::make('excerpt')
+ ->label(__('pages.excerpt_field'))
+ ->rows(3)
+ ->maxLength(500)
+ ->helperText(__('pages.excerpt_helper_text'))
+ ->columnSpanFull(),
+ ])
+ ->columnSpan(2)
+ ->collapsible(false);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/FooterTemplateSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/FooterTemplateSection.php
new file mode 100644
index 0000000..150fb23
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/FooterTemplateSection.php
@@ -0,0 +1,110 @@
+description(__('pages.form_section_footer_template_desc'))
+ ->schema([
+ Select::make('footer_template_id')
+ ->label(__('pages.footer_template_field'))
+ ->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
+ ->searchable()
+ ->live()
+ ->afterStateUpdated(function ($state, Set $set, Get $get) {
+ if (!$state) {
+ return;
+ }
+
+ $template = FooterTemplate::find($state);
+ if (!$template) {
+ return;
+ }
+
+ $defaultData = $template->default_data;
+ if (is_string($defaultData)) {
+ $defaultData = json_decode($defaultData, true) ?? [];
+ }
+ if (!is_array($defaultData) || empty($defaultData)) {
+ return;
+ }
+
+ $existingData = $get('footer_data') ?? [];
+
+ $flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
+ $result = [];
+ foreach ($array as $key => $value) {
+ $newKey = $prefix ? "{$prefix}.{$key}" : $key;
+ if (is_array($value)) {
+ $result = array_merge($result, $flattenDefaultData($value, $newKey));
+ } else {
+ $result[$newKey] = $value;
+ }
+ }
+ return $result;
+ };
+
+ $flattenedDefaults = $flattenDefaultData($defaultData);
+
+ foreach ($flattenedDefaults as $key => $defaultValue) {
+ $keys = explode('.', $key);
+ $currentValue = $existingData;
+
+ foreach ($keys as $k) {
+ $currentValue = $currentValue[$k] ?? null;
+ if ($currentValue === null) {
+ break;
+ }
+ }
+
+ if ($currentValue === null ||
+ $currentValue === '' ||
+ (is_array($currentValue) && empty($currentValue))) {
+ $set("footer_data.{$key}", $defaultValue);
+ }
+ }
+ })
+ ->columnSpanFull(),
+
+ Group::make()
+ ->schema(function (Get $get, $record): array {
+ $templateId = $get('footer_template_id');
+ if (!$templateId) {
+ return [];
+ }
+
+ $template = FooterTemplate::find($templateId);
+ if (!$template) {
+ return [];
+ }
+
+ $existingData = $record?->footer_data ?? [];
+ $templateDefaults = $template->default_data ?? [];
+
+ return TemplateService::generateDynamicFields(
+ $template,
+ 'footer_data',
+ $existingData,
+ $templateDefaults
+ );
+ })
+ ->visible(fn (Get $get): bool => filled($get('footer_template_id')))
+ ->columnSpanFull(),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(true);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/HeaderTemplateSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/HeaderTemplateSection.php
new file mode 100644
index 0000000..4c2ec67
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/HeaderTemplateSection.php
@@ -0,0 +1,110 @@
+description(__('pages.form_section_header_template_desc'))
+ ->schema([
+ Select::make('header_template_id')
+ ->label(__('pages.header_template_field'))
+ ->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
+ ->searchable()
+ ->live()
+ ->afterStateUpdated(function ($state, Set $set, Get $get) {
+ if (!$state) {
+ return;
+ }
+
+ $template = HeaderTemplate::find($state);
+ if (!$template) {
+ return;
+ }
+
+ $defaultData = $template->default_data;
+ if (is_string($defaultData)) {
+ $defaultData = json_decode($defaultData, true) ?? [];
+ }
+ if (!is_array($defaultData) || empty($defaultData)) {
+ return;
+ }
+
+ $existingData = $get('header_data') ?? [];
+
+ $flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
+ $result = [];
+ foreach ($array as $key => $value) {
+ $newKey = $prefix ? "{$prefix}.{$key}" : $key;
+ if (is_array($value)) {
+ $result = array_merge($result, $flattenDefaultData($value, $newKey));
+ } else {
+ $result[$newKey] = $value;
+ }
+ }
+ return $result;
+ };
+
+ $flattenedDefaults = $flattenDefaultData($defaultData);
+
+ foreach ($flattenedDefaults as $key => $defaultValue) {
+ $keys = explode('.', $key);
+ $currentValue = $existingData;
+
+ foreach ($keys as $k) {
+ $currentValue = $currentValue[$k] ?? null;
+ if ($currentValue === null) {
+ break;
+ }
+ }
+
+ if ($currentValue === null ||
+ $currentValue === '' ||
+ (is_array($currentValue) && empty($currentValue))) {
+ $set("header_data.{$key}", $defaultValue);
+ }
+ }
+ })
+ ->columnSpanFull(),
+
+ Group::make()
+ ->schema(function (Get $get, $record): array {
+ $templateId = $get('header_template_id');
+ if (!$templateId) {
+ return [];
+ }
+
+ $template = HeaderTemplate::find($templateId);
+ if (!$template) {
+ return [];
+ }
+
+ $existingData = $record?->header_data ?? [];
+ $templateDefaults = $template->default_data ?? [];
+
+ return TemplateService::generateDynamicFields(
+ $template,
+ 'header_data',
+ $existingData,
+ $templateDefaults
+ );
+ })
+ ->visible(fn (Get $get): bool => filled($get('header_template_id')))
+ ->columnSpanFull(),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(true);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/PageSettingsSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/PageSettingsSection.php
new file mode 100644
index 0000000..d6ccec4
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/PageSettingsSection.php
@@ -0,0 +1,98 @@
+schema([
+ FileUpload::make('featured_image')
+ ->label(__('pages.featured_image_field'))
+ ->image()
+ ->disk('public')
+ ->directory('pages/featured')
+ ->visibility('public')
+ ->imageEditor()
+ ->imageEditorAspectRatios([
+ '16:9',
+ '4:3',
+ '1:1',
+ ])
+ ->helperText(__('pages.featured_image_helper_text'))
+ ->columnSpanFull(),
+
+ Select::make('author_id')
+ ->label(__('pages.author_field'))
+ ->relationship('author', 'name')
+ ->default(auth()->id())
+ ->required()
+ ->columnSpanFull(),
+
+ DateTimePicker::make('published_at')
+ ->label(__('pages.published_at_field'))
+ ->displayFormat('d.m.Y H:i')
+ ->helperText(__('pages.published_at_helper_text'))
+ ->columnSpanFull(),
+
+ Select::make('status')
+ ->label(__('pages.status_field'))
+ ->options([
+ 'draft' => __('pages.status_draft'),
+ 'published' => __('pages.status_published'),
+ 'archived' => __('pages.status_archived'),
+ ])
+ ->default('draft')
+ ->required()
+ ->columnSpanFull(),
+
+ Select::make('parent_id')
+ ->label(__('pages.parent_field'))
+ ->relationship('parent', 'title')
+ ->searchable()
+ ->preload()
+ ->helperText(__('pages.parent_helper_text'))
+ ->columnSpanFull(),
+
+ Select::make('template')
+ ->label(__('pages.template_field'))
+ ->options([
+ 'default' => __('pages.template_default'),
+ 'landing' => __('pages.template_landing'),
+ 'blog' => __('pages.template_blog'),
+ 'contact' => __('pages.template_contact'),
+ ])
+ ->default('default')
+ ->columnSpanFull(),
+
+ TextInput::make('sort_order')
+ ->label(__('pages.sort_order_field'))
+ ->numeric()
+ ->default(0)
+ ->helperText(__('pages.sort_order_helper_text'))
+ ->columnSpanFull(),
+
+ Checkbox::make('is_homepage')
+ ->label(__('pages.is_homepage_field'))
+ ->helperText(__('pages.is_homepage_helper_text'))
+ ->columnSpanFull(),
+
+ Checkbox::make('show_in_menu')
+ ->label(__('pages.show_in_menu_field'))
+ ->default(true)
+ ->helperText(__('pages.show_in_menu_helper_text'))
+ ->columnSpanFull(),
+ ])
+ ->columnSpan(1)
+ ->collapsible(false);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionBuilderSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionBuilderSection.php
new file mode 100644
index 0000000..c2fb31b
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionBuilderSection.php
@@ -0,0 +1,376 @@
+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
+ 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(function ($component, $state, Get $get) {
+ if (!in_array($get('type'), ['text', 'url', 'email', 'phone', 'number'])) {
+ return;
+ }
+ $value = $get('value');
+ if ($value !== null) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Textarea
+ Textarea::make('_value_textarea')
+ ->label(__('pages.section_data_value'))
+ ->visible(fn (Get $get) => $get('type') === 'textarea')
+ ->rows(4)
+ ->placeholder('Metin girin...')
+ ->live(onBlur: true)
+ ->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
+ ->afterStateHydrated(function ($component, $state, Get $get) {
+ if ($get('type') !== 'textarea') {
+ return;
+ }
+ $value = $get('value');
+ if ($value !== null) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // HTML Code Editor
+ CodeEditor::make('_value_html')
+ ->label(__('pages.section_data_value'))
+ ->visible(fn (Get $get) => $get('type') === 'html')
+ ->language(Language::Html)
+ ->live(onBlur: true)
+ ->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
+ ->afterStateHydrated(function ($component, $state, Get $get) {
+ if ($get('type') !== 'html') {
+ return;
+ }
+ $value = $get('value');
+ if ($value !== null) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // JSON Code Editor
+ CodeEditor::make('_value_json')
+ ->label(__('pages.section_data_value'))
+ ->visible(fn (Get $get) => $get('type') === 'json')
+ ->language(Language::Json)
+ ->live(onBlur: true)
+ ->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
+ ->afterStateHydrated(function ($component, $state, Get $get) {
+ if ($get('type') !== 'json') {
+ return;
+ }
+ $value = $get('value');
+ if ($value !== null) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Rich Text Editor
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'richtext') {
+ return;
+ }
+
+ $value = $get('value');
+
+ if ($value === null || $value === '' || $value === []) {
+ return;
+ }
+
+ if (is_array($value) && isset($value['type']) && $value['type'] === 'doc') {
+ $component->state($value);
+ } elseif (is_string($value)) {
+ try {
+ $decoded = json_decode($value, true);
+ if (is_array($decoded) && isset($decoded['type']) && $decoded['type'] === 'doc') {
+ $component->state($decoded);
+ }
+ } catch (\Exception $e) {
+ // Invalid JSON, skip
+ }
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Markdown Editor
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'markdown') return;
+ $value = $get('value');
+ if ($value !== null && is_string($value)) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Image Upload
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'image') return;
+ $value = $get('value');
+ if ($value !== null && is_string($value)) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // File Upload
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'file') return;
+ $value = $get('value');
+ if ($value !== null && is_string($value)) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Boolean Toggle
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'boolean') return;
+ $value = $get('value');
+ if ($value !== null && is_bool($value)) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Color Picker
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'color') return;
+ $value = $get('value');
+ if ($value !== null && is_string($value)) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Date Picker
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'date') return;
+ $value = $get('value');
+ if ($value !== null && is_string($value)) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // DateTime Picker
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'datetime') return;
+ $value = $get('value');
+ if ($value !== null && is_string($value)) {
+ $component->state($value);
+ }
+ })
+ ->dehydrated(false)
+ ->columnSpanFull(),
+
+ // Array Repeater
+ 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(function ($component, $state, Get $get) {
+ if ($get('type') !== 'array') return;
+ $value = $get('value');
+ if ($value !== null && is_array($value)) {
+ $component->state($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()
+ ->collapsed()
+ ->itemLabel(fn (array $state): ?string => isset($state['type']) ? __('pages.section_type_' . $state['type']) : 'Bölüm')
+ ->columnSpanFull(),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(true);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionTemplatesSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionTemplatesSection.php
new file mode 100644
index 0000000..1260774
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionTemplatesSection.php
@@ -0,0 +1,132 @@
+description(__('pages.form_section_template_sections_desc'))
+ ->schema([
+ Repeater::make('sections_data')
+ ->label(__('pages.template_sections_field'))
+ ->schema([
+ Select::make('section_template_id')
+ ->label(__('pages.section_template_field'))
+ ->options(SectionTemplate::where('is_active', true)->pluck('title', 'id'))
+ ->searchable()
+ ->live()
+ ->required()
+ ->afterStateUpdated(function ($state, Set $set, Get $get) {
+ if (!$state) {
+ return;
+ }
+
+ $template = SectionTemplate::find($state);
+ if (!$template) {
+ return;
+ }
+
+ $defaultData = $template->default_data;
+ if (is_string($defaultData)) {
+ $defaultData = json_decode($defaultData, true) ?? [];
+ }
+ if (!is_array($defaultData) || empty($defaultData)) {
+ return;
+ }
+
+ $existingData = $get('section_data') ?? [];
+
+ $flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
+ $result = [];
+ foreach ($array as $key => $value) {
+ $newKey = $prefix ? "{$prefix}.{$key}" : $key;
+ if (is_array($value)) {
+ $result = array_merge($result, $flattenDefaultData($value, $newKey));
+ } else {
+ $result[$newKey] = $value;
+ }
+ }
+ return $result;
+ };
+
+ $flattenedDefaults = $flattenDefaultData($defaultData);
+
+ foreach ($flattenedDefaults as $key => $defaultValue) {
+ $keys = explode('.', $key);
+ $currentValue = $existingData;
+
+ foreach ($keys as $k) {
+ $currentValue = $currentValue[$k] ?? null;
+ if ($currentValue === null) {
+ break;
+ }
+ }
+
+ if ($currentValue === null ||
+ $currentValue === '' ||
+ (is_array($currentValue) && empty($currentValue))) {
+ $set("section_data.{$key}", $defaultValue);
+ }
+ }
+ })
+ ->columnSpanFull(),
+
+ Group::make()
+ ->schema(function (Get $get, $state): array {
+ $templateId = $get('section_template_id');
+ if (!$templateId) {
+ return [];
+ }
+
+ $template = SectionTemplate::find($templateId);
+ if (!$template) {
+ return [];
+ }
+
+ $existingData = $state?->section_data ?? [];
+ $templateDefaults = $template->default_data ?? [];
+
+ return TemplateService::generateDynamicFields(
+ $template,
+ 'section_data',
+ $existingData,
+ $templateDefaults
+ );
+ })
+ ->visible(fn (Get $get): bool => filled($get('section_template_id')))
+ ->columnSpanFull(),
+ ])
+ ->defaultItems(0)
+ ->addActionLabel(__('pages.add_template_section'))
+ ->reorderable()
+ ->collapsible()
+ ->collapsed()
+ ->itemLabel(function (array $state): ?string {
+ if (!isset($state['section_template_id'])) {
+ return __('pages.template_section');
+ }
+
+ $template = SectionTemplate::find($state['section_template_id']);
+ return $template ? $template->title : __('pages.template_section');
+ })
+
+ ->columnSpanFull(),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(true);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/SeoSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SeoSection.php
new file mode 100644
index 0000000..62123dd
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SeoSection.php
@@ -0,0 +1,33 @@
+schema([
+ TextInput::make('meta_title')
+ ->label(__('pages.meta_title_field'))
+ ->maxLength(60)
+ ->helperText(__('pages.meta_title_helper_text')),
+
+ Textarea::make('meta_description')
+ ->label(__('pages.meta_description_field'))
+ ->rows(3)
+ ->maxLength(160)
+ ->helperText(__('pages.meta_description_helper_text'))
+ ->columnSpanFull(),
+ ])
+ ->columns(2)
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(true);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/TranslationsSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/TranslationsSection.php
new file mode 100644
index 0000000..a8d7328
--- /dev/null
+++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/TranslationsSection.php
@@ -0,0 +1,53 @@
+schema([
+ TranslationTabs::make([
+ 'title' => [
+ 'type' => 'text',
+ 'label' => __('pages.title_field'),
+ 'required' => false,
+ 'maxLength' => 255,
+ ],
+ 'content' => [
+ 'type' => 'richtext',
+ 'label' => __('pages.content_field'),
+ 'required' => false,
+ ],
+ 'excerpt' => [
+ 'type' => 'textarea',
+ 'label' => __('pages.excerpt_field'),
+ 'required' => false,
+ 'maxLength' => 500,
+ 'rows' => 3,
+ ],
+ 'meta_title' => [
+ 'type' => 'text',
+ 'label' => __('pages.meta_title_field'),
+ 'required' => false,
+ 'maxLength' => 60,
+ ],
+ 'meta_description' => [
+ 'type' => 'textarea',
+ 'label' => __('pages.meta_description_field'),
+ 'required' => false,
+ 'maxLength' => 160,
+ 'rows' => 3,
+ ],
+ ]),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(false);
+ }
+}
+
diff --git a/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php b/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php
index 54bae22..769df06 100644
--- a/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php
+++ b/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php
@@ -21,12 +21,7 @@ class PagesTable
{
return $table
->columns([
- ImageColumn::make('featured_image_url')
- ->label(__('pages.table_column_featured_image'))
- ->circular()
- ->size(40)
- ->defaultImageUrl('/images/placeholder-page.png'),
-
+
TextColumn::make('title')
->label(__('pages.table_column_title'))
->searchable()
diff --git a/app/Filament/Admin/Resources/SectionTemplates/Pages/CreateSectionTemplate.php b/app/Filament/Admin/Resources/SectionTemplates/Pages/CreateSectionTemplate.php
new file mode 100644
index 0000000..baf1d64
--- /dev/null
+++ b/app/Filament/Admin/Resources/SectionTemplates/Pages/CreateSectionTemplate.php
@@ -0,0 +1,41 @@
+ $value) {
+ if (str_starts_with($key, 'default_data.')) {
+ // Remove 'default_data.' prefix to get the nested key
+ $nestedKey = substr($key, 13); // 'default_data.' length is 13
+ $defaultData[$nestedKey] = $value;
+ // Remove the flattened key from data
+ unset($data[$key]);
+ }
+ }
+
+ // Set the properly formatted default_data array
+ if (!empty($defaultData)) {
+ $data['default_data'] = $defaultData;
+ }
+
+ return $data;
+ }
+}
diff --git a/app/Filament/Admin/Resources/SectionTemplates/Pages/EditSectionTemplate.php b/app/Filament/Admin/Resources/SectionTemplates/Pages/EditSectionTemplate.php
new file mode 100644
index 0000000..d48b122
--- /dev/null
+++ b/app/Filament/Admin/Resources/SectionTemplates/Pages/EditSectionTemplate.php
@@ -0,0 +1,62 @@
+ $value) {
+ if (str_starts_with($key, 'default_data.')) {
+ // Remove 'default_data.' prefix to get the nested key
+ $nestedKey = substr($key, 13); // 'default_data.' length is 13
+ $defaultData[$nestedKey] = $value;
+ // Remove the flattened key from data
+ unset($data[$key]);
+ }
+ }
+
+ // Set the properly formatted default_data array
+ if (!empty($defaultData)) {
+ $data['default_data'] = $defaultData;
+ }
+
+ return $data;
+ }
+
+ protected function afterSave(): void
+ {
+ // Kaydetme sonrası preview'ı güncelle
+ // Livewire event dispatch et
+ $this->dispatch('template-saved');
+ }
+}
diff --git a/app/Filament/Admin/Resources/SectionTemplates/Pages/ListSectionTemplates.php b/app/Filament/Admin/Resources/SectionTemplates/Pages/ListSectionTemplates.php
new file mode 100644
index 0000000..4788454
--- /dev/null
+++ b/app/Filament/Admin/Resources/SectionTemplates/Pages/ListSectionTemplates.php
@@ -0,0 +1,19 @@
+components([
+ Section::make(__('section-templates.section_general'))
+ ->schema([
+ TextInput::make('title')
+ ->label(__('section-templates.field_title'))
+ ->required()
+ ->maxLength(255)
+ ->columnSpanFull(),
+
+ // Placeholder Picker Component
+ View::make('components.placeholder-picker')
+ ->viewData([
+ 'fieldName' => 'html_content',
+ ])
+ ->columnSpanFull(),
+
+ CodeEditor::make('html_content')
+ ->label(__('section-templates.field_html_content'))
+ ->required()
+ ->live(onBlur: false) // Real-time updates
+ ->language(Language::Html)
+ ->columnSpanFull()
+ ->helperText(__('section-templates.field_html_content_help'))
+ ->extraAttributes([
+ 'style' => 'max-height: 400px;overflow-y: auto;',
+ 'data-field-name' => 'html_content',
+ ]),
+
+ // Preview Component - placed after editor
+ View::make('components.template-preview')
+ ->viewData([
+ 'type' => 'section',
+ 'fieldName' => 'html_content',
+ 'recordId' => null, // Will be extracted from URL by JavaScript
+ ])
+ ->columnSpanFull(),
+
+ Toggle::make('is_active')
+ ->label(__('section-templates.field_is_active'))
+ ->default(true)
+ ->inline(false),
+ ])
+ ->columnSpanFull(),
+
+ // Default Data Section - Dynamic fields based on placeholders
+ Section::make(__('section-templates.section_default_data'))
+ ->description(__('section-templates.section_default_data_desc'))
+ ->schema([
+ Group::make()
+ ->schema(function (Get $get, $record): array {
+ $htmlContent = $get('html_content');
+ if (empty($htmlContent)) {
+ return [];
+ }
+
+ // Create a temporary template object to parse placeholders
+ $tempTemplate = new SectionTemplate();
+ $tempTemplate->html_content = $htmlContent;
+
+ // Get existing default_data if editing
+ $existingData = $record ? ($record->default_data ?? []) : [];
+
+ return TemplateService::generateDynamicFields(
+ $tempTemplate,
+ 'default_data',
+ $existingData,
+ null // No fallback needed for template's own default_data
+ );
+ })
+ ->key('default_data_fields')
+ ->visible(fn (Get $get): bool => filled($get('html_content')))
+ ->columnSpanFull(),
+ ])
+ ->columnSpanFull()
+ ->collapsible(true)
+ ->collapsed(false),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateInfolist.php b/app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateInfolist.php
new file mode 100644
index 0000000..d67f1f8
--- /dev/null
+++ b/app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateInfolist.php
@@ -0,0 +1,16 @@
+components([
+ //
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/SectionTemplates/SectionTemplateResource.php b/app/Filament/Admin/Resources/SectionTemplates/SectionTemplateResource.php
new file mode 100644
index 0000000..59ca77e
--- /dev/null
+++ b/app/Filament/Admin/Resources/SectionTemplates/SectionTemplateResource.php
@@ -0,0 +1,86 @@
+ ListSectionTemplates::route('/'),
+ 'create' => CreateSectionTemplate::route('/create'),
+ 'view' => ViewSectionTemplate::route('/{record}'),
+ 'edit' => EditSectionTemplate::route('/{record}/edit'),
+ ];
+ }
+
+ public static function getRecordRouteBindingEloquentQuery(): Builder
+ {
+ return parent::getRecordRouteBindingEloquentQuery()
+ ->withoutGlobalScopes([
+ SoftDeletingScope::class,
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/SectionTemplates/Tables/SectionTemplatesTable.php b/app/Filament/Admin/Resources/SectionTemplates/Tables/SectionTemplatesTable.php
new file mode 100644
index 0000000..9e3f887
--- /dev/null
+++ b/app/Filament/Admin/Resources/SectionTemplates/Tables/SectionTemplatesTable.php
@@ -0,0 +1,65 @@
+columns([
+ TextColumn::make('title')
+ ->label(__('section-templates.column_title'))
+ ->searchable()
+ ->sortable(),
+
+ IconColumn::make('is_active')
+ ->label(__('section-templates.column_is_active'))
+ ->boolean()
+ ->sortable(),
+
+ TextColumn::make('created_at')
+ ->label(__('section-templates.column_created_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('updated_at')
+ ->label(__('section-templates.column_updated_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+
+ TextColumn::make('deleted_at')
+ ->label(__('section-templates.column_deleted_at'))
+ ->dateTime()
+ ->sortable()
+ ->toggleable(isToggledHiddenByDefault: true),
+ ])
+ ->filters([
+ TrashedFilter::make(),
+ ])
+ ->recordActions([
+ ViewAction::make(),
+ EditAction::make(),
+ ])
+ ->toolbarActions([
+ BulkActionGroup::make([
+ DeleteBulkAction::make(),
+ ForceDeleteBulkAction::make(),
+ RestoreBulkAction::make(),
+ ]),
+ ]);
+ }
+}
diff --git a/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php b/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php
index 928b25a..57a47ae 100644
--- a/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php
+++ b/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php
@@ -3,6 +3,7 @@
namespace App\Filament\Admin\Resources\Settings\Pages;
use App\Filament\Admin\Resources\Settings\SettingResource;
+use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
@@ -20,14 +21,33 @@ class EditSetting extends EditRecord
protected function getHeaderActions(): array
{
return [
+ Action::make('save')
+ ->label(__('settings.save'))
+ ->action('save')
+ ->keyBindings(['mod+s'])
+ ->color('primary')
+ ->size('sm'),
+ Action::make('cancel')
+ ->label(__('settings.cancel'))
+ ->url($this->getResource()::getUrl('index'))
+ ->color('gray')
+ ->size('sm'),
DeleteAction::make()
- ->label(__('settings.delete')),
+ ->label(__('settings.delete'))
+ ->size('sm'),
RestoreAction::make()
- ->label(__('settings.restore')),
+ ->label(__('settings.restore'))
+ ->size('sm'),
ForceDeleteAction::make()
- ->label(__('settings.force_delete')),
+ ->label(__('settings.force_delete'))
+ ->size('sm'),
];
}
+
+ protected function getFormActions(): array
+ {
+ return []; // Boş array döndürerek alt taraftaki form action'larını gizliyoruz
+ }
protected function getRedirectUrl(): string
{
diff --git a/app/Filament/Admin/Widgets/PagesMenuWidget.php b/app/Filament/Admin/Widgets/PagesMenuWidget.php
new file mode 100644
index 0000000..ccd5ebb
--- /dev/null
+++ b/app/Filament/Admin/Widgets/PagesMenuWidget.php
@@ -0,0 +1,112 @@
+where('status', 'published')
+ ->where('show_in_menu', true)
+ ->orderBy('sort_order');
+ }
+
+ public function getTreeRecordTitle(?\Illuminate\Database\Eloquent\Model $record = null): string
+ {
+ if (!$record) {
+ return '';
+ }
+
+ $title = $record->title;
+
+ // Add child count indicator
+ $childCount = $record->children()
+ ->where('status', 'published')
+ ->where('show_in_menu', true)
+ ->count();
+
+ if ($childCount > 0) {
+ $title .= " ({$childCount})";
+ }
+
+ return $title;
+ }
+
+ public function getTreeRecordIcon(?\Illuminate\Database\Eloquent\Model $record = null): ?string
+ {
+ if (!$record) {
+ return null;
+ }
+
+ // Root pages get folder icon (parent_id is null)
+ if ($record->parent_id === null) {
+ return 'heroicon-o-folder';
+ }
+
+ // Homepage gets special icon
+ if ($record->is_homepage) {
+ return 'heroicon-o-home';
+ }
+
+ // Child pages get document icon
+ return 'heroicon-o-document-text';
+ }
+
+ protected function hasDeleteAction(): bool
+ {
+ return false;
+ }
+
+ protected function hasEditAction(): bool
+ {
+ return true;
+ }
+
+ protected function hasViewAction(): bool
+ {
+ return true;
+ }
+
+ protected function configureEditAction(EditAction $action): EditAction
+ {
+ return $action
+ ->icon('heroicon-o-pencil')
+ ->label(__('pages.edit'))
+ ->url(fn (Page $record): string => PageResource::getUrl('edit', ['record' => $record]))
+ ->openUrlInNewTab(false);
+ }
+
+ protected function configureViewAction(ViewAction $action): ViewAction
+ {
+ return $action
+ ->icon('heroicon-o-eye')
+ ->label(__('pages.view'))
+ ->url(fn (Page $record): string => $record->url)
+ ->openUrlInNewTab(true);
+ }
+
+ public function getNodeCollapsedState(?\Illuminate\Database\Eloquent\Model $record = null): bool
+ {
+ return true;
+ }
+}
+
diff --git a/app/Helpers/translation_helpers.php b/app/Helpers/translation_helpers.php
index d71464b..02dee5d 100644
--- a/app/Helpers/translation_helpers.php
+++ b/app/Helpers/translation_helpers.php
@@ -181,10 +181,7 @@ if (!function_exists('switch_language')) {
}
app()->setLocale($languageCode);
-
- if (auth()->check()) {
- session(['locale' => $languageCode]);
- }
+ session(['locale' => $languageCode]);
return true;
}
diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php
new file mode 100644
index 0000000..381d298
--- /dev/null
+++ b/app/Http/Controllers/BlogController.php
@@ -0,0 +1,47 @@
+first()) : null;
+ $posts = class_exists(Post::class)
+ ? Post::query()->where('status', 'published')->latest('published_at')->paginate(12)
+ : collect();
+
+ return view('blog.index', [
+ 'settings' => $settings,
+ 'posts' => $posts,
+ 'meta' => [
+ 'title' => __('blog.meta-index-title'),
+ 'description' => __('blog.meta-index-description'),
+ ],
+ ]);
+ }
+
+ public function show(string $slug)
+ {
+ $settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
+ if (!class_exists(Post::class)) {
+ abort(404);
+ }
+ $post = Post::query()->where('slug', $slug)->where('status', 'published')->firstOrFail();
+
+ return view('blog.show', [
+ 'settings' => $settings,
+ 'post' => $post,
+ 'meta' => [
+ 'title' => method_exists($post, 'translate') ? ($post->translate('meta_title') ?: $post->translate('title')) : ($post->meta_title ?? $post->title),
+ 'description' => method_exists($post, 'translate') ? ($post->translate('meta_description') ?: $post->translate('excerpt')) : ($post->meta_description ?? $post->excerpt),
+ 'image' => $post->featured_image ?? null,
+ ],
+ ]);
+ }
+}
+
+
diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php
index 85ab2f7..de6fbf0 100644
--- a/app/Http/Controllers/PageController.php
+++ b/app/Http/Controllers/PageController.php
@@ -3,6 +3,8 @@
namespace App\Http\Controllers;
use App\Models\Page;
+use App\Models\Setting;
+use App\Services\TemplateService;
use Illuminate\Http\Request;
class PageController extends Controller
@@ -12,37 +14,176 @@ class PageController extends Controller
*/
public function index()
{
- // Homepage'i bul
- $page = Page::where('is_homepage', true)
+ // 1) is_homepage işaretli ve published (en son güncellenen)
+ $page = Page::with(['headerTemplate', 'footerTemplate'])
+ ->where('is_homepage', true)
->where('status', 'published')
+ ->latest('updated_at')
->first();
+
+
+
+ $settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
+
+ // Hiç sayfa yoksa: direkt home template'inin statik fallback'i ile render et
if (!$page) {
- // Homepage yoksa, view'e boş geçelim
- return view('home', [
+ return view('templates.home', [
'page' => null,
- 'menuItems' => $this->getMenuItems()
+ 'settings' => $settings,
+ 'meta' => [
+ 'title' => $settings->default_meta_title ?? config('app.name'),
+ 'description' => $settings->default_meta_description ?? null,
+ 'image' => $settings->default_meta_image ?? null,
+ ],
]);
}
- return view('home', [
+ $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;
+ if ($page->headerTemplate) {
+ // Merge template defaults with page data (page data overrides defaults)
+ $templateDefaults = $page->headerTemplate->default_data ?? [];
+ $pageData = $page->header_data ?? [];
+ $mergedHeaderData = array_merge($templateDefaults, $pageData);
+
+ $renderedHeader = TemplateService::replacePlaceholders(
+ $page->headerTemplate->html_content,
+ $mergedHeaderData
+ );
+ }
+
+ // Render Footer Template
+ $renderedFooter = null;
+ if ($page->footerTemplate) {
+ // Merge template defaults with page data (page data overrides defaults)
+ $templateDefaults = $page->footerTemplate->default_data ?? [];
+ $pageData = $page->footer_data ?? [];
+ $mergedFooterData = array_merge($templateDefaults, $pageData);
+
+ $renderedFooter = TemplateService::replacePlaceholders(
+ $page->footerTemplate->html_content,
+ $mergedFooterData
+ );
+ }
+
+ return view($view, [
'page' => $page,
- 'menuItems' => $this->getMenuItems()
+ '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,
+ ],
]);
}
/**
* Display a specific page by slug
*/
- public function show($slug)
+public function show($slug)
{
- $page = Page::where('slug', $slug)
+ $page = Page::with(['headerTemplate', 'footerTemplate'])
+ ->where('slug', $slug)
->where('status', 'published')
->firstOrFail();
- return view('page', [
+ $settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
+
+ $metaTitle = method_exists($page, 'translate')
+ ? ($page->translate('meta_title') ?: $page->translate('title'))
+ : ($page->meta_title ?? $page->title ?? null);
+
+ $metaDescription = method_exists($page, 'translate')
+ ? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
+ : ($page->meta_description ?? $page->excerpt ?? null);
+
+ // 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
+ $template = $page->template
+ ?? (($slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
+
+ $view = view()->exists("templates.$template")
+ ? "templates.$template"
+ : 'templates.generic';
+
+ // Render Header Template
+ $renderedHeader = null;
+ if ($page->headerTemplate) {
+ // Merge template defaults with page data (page data overrides defaults)
+ $templateDefaults = $page->headerTemplate->default_data ?? [];
+ $pageData = $page->header_data ?? [];
+ $mergedHeaderData = array_merge($templateDefaults, $pageData);
+
+ $renderedHeader = TemplateService::replacePlaceholders(
+ $page->headerTemplate->html_content,
+ $mergedHeaderData
+ );
+ }
+
+ // Render Footer Template
+ $renderedFooter = null;
+ if ($page->footerTemplate) {
+ // Merge template defaults with page data (page data overrides defaults)
+ $templateDefaults = $page->footerTemplate->default_data ?? [];
+ $pageData = $page->footer_data ?? [];
+ $mergedFooterData = array_merge($templateDefaults, $pageData);
+
+ $renderedFooter = TemplateService::replacePlaceholders(
+ $page->footerTemplate->html_content,
+ $mergedFooterData
+ );
+ }
+
+ return view($view, [
'page' => $page,
- 'menuItems' => $this->getMenuItems()
+ '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,
+ ],
]);
}
diff --git a/app/Http/Controllers/TemplatePreviewController.php b/app/Http/Controllers/TemplatePreviewController.php
new file mode 100644
index 0000000..6b66ddd
--- /dev/null
+++ b/app/Http/Controllers/TemplatePreviewController.php
@@ -0,0 +1,79 @@
+input('record_id');
+ $type = $request->input('type', 'section'); // header, footer, section
+
+ // Validate type
+ if (!in_array($type, ['header', 'footer', 'section', 'menu'])) {
+ $type = 'section';
+ }
+
+ // Get template and its default data
+ $content = '';
+ $defaultData = [];
+
+ if ($recordId) {
+ $template = $this->getTemplate($recordId, $type);
+ if ($template) {
+ $content = $template->html_content ?? '';
+ $defaultData = $template->default_data ?? [];
+ }
+ } else {
+ // Fallback: eski yöntem (content gönderimi)
+ $content = $request->input('content', '');
+ }
+
+ if (empty($content)) {
+ return response('No content to preview', 400);
+ }
+
+ // Replace placeholders with default data
+ // Always call replacePlaceholders to handle custom components and special placeholders
+ // even if defaultData is empty
+ $content = TemplateService::replacePlaceholders($content, $defaultData ?? []);
+
+ $html = TemplatePreviewService::getPreviewHtml($content, $type);
+
+ return response($html)
+ ->header('Content-Type', 'text/html; charset=utf-8');
+ }
+
+ /**
+ * Get template object from database by ID
+ *
+ * @param int $id
+ * @param string $type
+ * @return object|null
+ */
+ private function getTemplate(int $id, string $type): ?object
+ {
+ return match($type) {
+ 'header' => HeaderTemplate::find($id),
+ 'footer' => FooterTemplate::find($id),
+ 'section' => SectionTemplate::find($id),
+ 'menu' => MenuTemplate::find($id),
+ default => null,
+ };
+ }
+}
+
diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php
new file mode 100644
index 0000000..432bc9d
--- /dev/null
+++ b/app/Http/Middleware/SetLocale.php
@@ -0,0 +1,52 @@
+ 'boolean',
+ 'default_data' => 'array',
+ ];
+
+ /**
+ * Get the pages that use this footer template.
+ */
+ public function pages(): HasMany
+ {
+ return $this->hasMany(Page::class, 'footer_template_id');
+ }
+}
diff --git a/app/Models/HeaderTemplate.php b/app/Models/HeaderTemplate.php
new file mode 100644
index 0000000..e63ec54
--- /dev/null
+++ b/app/Models/HeaderTemplate.php
@@ -0,0 +1,32 @@
+ 'boolean',
+ 'default_data' => 'array',
+ ];
+
+ /**
+ * Get the pages that use this header template.
+ */
+ public function pages(): HasMany
+ {
+ return $this->hasMany(Page::class, 'header_template_id');
+ }
+}
diff --git a/app/Models/MenuTemplate.php b/app/Models/MenuTemplate.php
new file mode 100644
index 0000000..673ccf7
--- /dev/null
+++ b/app/Models/MenuTemplate.php
@@ -0,0 +1,21 @@
+ 'boolean',
+ ];
+}
diff --git a/app/Models/Page.php b/app/Models/Page.php
index e039d07..38fe29b 100644
--- a/app/Models/Page.php
+++ b/app/Models/Page.php
@@ -2,14 +2,16 @@
namespace App\Models;
+use App\Models\SectionTemplate;
use App\Traits\HasTranslations;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
+use SolutionForest\FilamentTree\Concern\ModelTree;
class Page extends Model
{
- use HasFactory, SoftDeletes, HasTranslations;
+ use HasFactory, SoftDeletes, HasTranslations, ModelTree;
protected $fillable = [
'title',
@@ -27,6 +29,14 @@ class Page extends Model
'template',
'is_homepage',
'show_in_menu',
+ 'sections',
+ 'data',
+ // Template System
+ 'header_template_id',
+ 'header_data',
+ 'footer_template_id',
+ 'footer_data',
+ 'sections_data',
];
/**
@@ -45,6 +55,12 @@ class Page extends Model
'is_homepage' => 'boolean',
'show_in_menu' => 'boolean',
'sort_order' => 'integer',
+ 'sections' => 'array',
+ 'data' => 'array',
+ // Template System
+ 'header_data' => 'array',
+ 'footer_data' => 'array',
+ 'sections_data' => 'array',
];
public function author()
@@ -62,6 +78,22 @@ class Page extends Model
return $this->hasMany(Page::class, 'parent_id');
}
+ /**
+ * Get the header template for the page.
+ */
+ public function headerTemplate()
+ {
+ return $this->belongsTo(HeaderTemplate::class, 'header_template_id');
+ }
+
+ /**
+ * Get the footer template for the page.
+ */
+ public function footerTemplate()
+ {
+ return $this->belongsTo(FooterTemplate::class, 'footer_template_id');
+ }
+
public function getRouteKeyName()
{
return 'slug';
@@ -84,4 +116,84 @@ 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;
+ }
+
+ /**
+ * Get templated sections with their templates loaded.
+ * Used for new dynamic template system.
+ */
+ public function getTemplatedSectionsAttribute()
+ {
+ if (!$this->sections_data || !is_array($this->sections_data)) {
+ return collect([]);
+ }
+
+ return collect($this->sections_data)->map(function ($section) {
+ $templateId = $section['section_template_id'] ?? null;
+ $template = $templateId ? SectionTemplate::find($templateId) : null;
+
+ return [
+ 'template' => $template,
+ 'data' => $section['section_data'] ?? [],
+ ];
+ })->filter(fn($section) => $section['template'] !== null);
+ }
+
+ /**
+ * ModelTree - Custom column names
+ */
+ public function determineOrderColumnName(): string
+ {
+ return 'sort_order';
+ }
+
+ public function determineParentColumnName(): string
+ {
+ return 'parent_id';
+ }
+
+ public function determineTitleColumnName(): string
+ {
+ return 'title';
+ }
+
+ public static function defaultParentKey()
+ {
+ return null; // Mevcut migration nullable olduğu için null kullanıyoruz
+ }
}
diff --git a/app/Models/SectionTemplate.php b/app/Models/SectionTemplate.php
new file mode 100644
index 0000000..f8d6bff
--- /dev/null
+++ b/app/Models/SectionTemplate.php
@@ -0,0 +1,32 @@
+ 'boolean',
+ 'default_data' => 'array',
+ ];
+
+ /**
+ * Section templates are used via JSON in pages.sections_data
+ * No direct relation needed, but we can add a helper method
+ */
+ public function getPagesUsingThisTemplate()
+ {
+ return Page::whereJsonContains('sections_data', [['section_template_id' => $this->id]])->get();
+ }
+}
diff --git a/app/Observers/PageObserver.php b/app/Observers/PageObserver.php
new file mode 100644
index 0000000..6faa559
--- /dev/null
+++ b/app/Observers/PageObserver.php
@@ -0,0 +1,26 @@
+is_homepage) {
+ // Diğer tüm sayfaların homepage işaretini kaldır
+ Page::where('id', '!=', $page->id)
+ ->where('is_homepage', true)
+ ->update(['is_homepage' => false]);
+ }
+ }
+}
+
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 452e6b6..ec8ef22 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -2,6 +2,8 @@
namespace App\Providers;
+use App\Models\Page;
+use App\Observers\PageObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
@@ -19,6 +21,7 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
- //
+ // Page model için observer kaydet
+ Page::observe(PageObserver::class);
}
}
diff --git a/app/Services/GuideService.php b/app/Services/GuideService.php
new file mode 100644
index 0000000..19819d6
--- /dev/null
+++ b/app/Services/GuideService.php
@@ -0,0 +1,108 @@
+guidePath = resource_path('views/guide');
+ }
+
+ /**
+ * Tüm markdown dosyalarını son güncellenme tarihine göre sıralı olarak getir
+ *
+ * @return array
+ */
+ public function getAllGuides(): array
+ {
+ if (!File::exists($this->guidePath)) {
+ return [];
+ }
+
+ $files = File::glob($this->guidePath . '/*.md');
+
+ $guides = [];
+
+ foreach ($files as $file) {
+ $filename = File::basename($file);
+ $name = Str::before($filename, '.md');
+ $modifiedTime = File::lastModified($file);
+
+ $guides[] = [
+ 'filename' => $filename,
+ 'name' => $name,
+ 'slug' => Str::slug($name),
+ 'path' => $file,
+ 'modified_time' => $modifiedTime,
+ 'modified_date' => date('Y-m-d H:i:s', $modifiedTime),
+ ];
+ }
+
+ // Son güncellenenden ilk güncellenene göre sırala
+ usort($guides, function ($a, $b) {
+ return $b['modified_time'] <=> $a['modified_time'];
+ });
+
+ return $guides;
+ }
+
+ /**
+ * Belirli bir guide dosyasını getir
+ *
+ * @param string $slug
+ * @return array|null
+ */
+ public function getGuide(string $slug): ?array
+ {
+ $guides = $this->getAllGuides();
+
+ foreach ($guides as $guide) {
+ if ($guide['slug'] === $slug) {
+ $guide['content'] = File::get($guide['path']);
+ return $guide;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * İlk guide'ı getir (varsayılan olarak gösterilecek)
+ *
+ * @return array|null
+ */
+ public function getFirstGuide(): ?array
+ {
+ $guides = $this->getAllGuides();
+
+ if (empty($guides)) {
+ return null;
+ }
+
+ $firstGuide = $guides[0];
+ $firstGuide['content'] = File::get($firstGuide['path']);
+
+ return $firstGuide;
+ }
+
+ /**
+ * Guide dosyasının var olup olmadığını kontrol et
+ *
+ * @param string $slug
+ * @return bool
+ */
+ public function guideExists(string $slug): bool
+ {
+ return $this->getGuide($slug) !== null;
+ }
+}
+
diff --git a/app/Services/MenuService.php b/app/Services/MenuService.php
new file mode 100644
index 0000000..f8a2e22
--- /dev/null
+++ b/app/Services/MenuService.php
@@ -0,0 +1,128 @@
+ function ($query) {
+ $query->where('status', 'published')
+ ->where('show_in_menu', true)
+ ->orderBy('sort_order', 'asc')
+ ->orderBy('title', 'asc');
+ }])
+ ->whereNull('parent_id')
+ ->where('status', 'published')
+ ->where('show_in_menu', true)
+ ->orderBy('sort_order', 'asc')
+ ->orderBy('title', 'asc')
+ ->get();
+ });
+
+ // Get menu template from Settings
+ $menuTemplateId = Setting::get('menu_template_id');
+
+ if ($menuTemplateId) {
+ $menuTemplate = MenuTemplate::where('is_active', true)->find($menuTemplateId);
+
+ if ($menuTemplate && !empty($menuTemplate->html_content)) {
+ $html = $menuTemplate->html_content;
+
+ // Replace {menu} placeholder with rendered menu structure
+ $renderedMenu = self::buildMenuStructure($menuItems);
+ $html = str_replace('{menu}', $renderedMenu, $html);
+
+ return $html;
+ }
+ }
+
+ // Otherwise, return default menu structure
+ return self::buildMenuStructure($menuItems);
+ }
+
+ /**
+ * Build menu structure HTML from menu items
+ *
+ * @param \Illuminate\Support\Collection $menuItems
+ * @return string HTML structure
+ */
+ protected static function buildMenuStructure($menuItems): string
+ {
+ if ($menuItems->isEmpty()) {
+ return '';
+ }
+
+ $html = '
';
+
+ return $html;
+ }
+
+ /**
+ * Build submenu structure recursively
+ *
+ * @param \Illuminate\Support\Collection $children
+ * @return string Submenu HTML
+ */
+ protected static function buildSubmenu($children): string
+ {
+ $html = '';
+
+ return $html;
+ }
+
+ /**
+ * Clear menu cache
+ */
+ public static function clearCache(): void
+ {
+ Cache::forget('menu_items');
+ }
+}
+
diff --git a/app/Services/TemplatePreviewService.php b/app/Services/TemplatePreviewService.php
new file mode 100644
index 0000000..6634986
--- /dev/null
+++ b/app/Services/TemplatePreviewService.php
@@ -0,0 +1,240 @@
+ $settingKey) {
+ if (str_starts_with($assetKey, 'css_')) {
+ $setting = Setting::get($settingKey);
+ if ($setting) {
+ $configKey = str_replace('css_', '', $assetKey);
+ $config[$configKey] = $setting;
+ }
+ }
+ }
+ }
+
+ return $config;
+ }
+
+ /**
+ * Get all JavaScript assets for preview
+ *
+ * @return array
+ */
+ public static function getJsAssets(): array
+ {
+ $config = config('template-preview.js', []);
+
+ if (config('template-preview.use_settings_override', false)) {
+ $settingsKeys = config('template-preview.settings_keys', []);
+
+ foreach ($settingsKeys as $assetKey => $settingKey) {
+ if (str_starts_with($assetKey, 'js_')) {
+ $setting = Setting::get($settingKey);
+ if ($setting) {
+ $configKey = str_replace('js_', '', $assetKey);
+ $config[$configKey] = $setting;
+ }
+ }
+ }
+ }
+
+ return $config;
+ }
+
+ /**
+ * Get formatted CSS link tags
+ *
+ * @return string
+ */
+ public static function getCssLinks(): string
+ {
+ $assets = self::getCssAssets();
+ $html = '';
+
+ foreach ($assets as $key => $path) {
+ if ($key === 'fonts') {
+ // Preload for fonts
+ $html .= sprintf(
+ '' . "\n",
+ asset($path)
+ );
+ } else {
+ $html .= sprintf(
+ '' . "\n",
+ asset($path)
+ );
+ }
+ }
+
+ return $html;
+ }
+
+ /**
+ * Get formatted JavaScript script tags
+ *
+ * @return string
+ */
+ public static function getJsScripts(): string
+ {
+ $assets = self::getJsAssets();
+ $html = '';
+
+ foreach ($assets as $path) {
+ $html .= sprintf(
+ '' . "\n",
+ asset($path)
+ );
+ }
+
+ return $html;
+ }
+
+ /**
+ * Convert relative asset paths to absolute URLs in HTML content
+ *
+ * @param string $html The HTML content
+ * @return string HTML with absolute URLs
+ */
+ public static function convertRelativePathsToAbsolute(string $html): string
+ {
+ // Base URL for assets
+ $baseUrl = rtrim(url('/'), '/');
+
+ // Pattern 1: Match attributes with relative paths (src, href, data-src, etc.)
+ // Matches: src="assets/...", href="assets/...", data-src="assets/..."
+ // Also matches: src="/assets/...", href="./assets/...", etc.
+ $html = preg_replace_callback(
+ '/(\s+(?:src|href|data-src|data-href|srcset|action|poster|background)\s*=\s*["\'])((?:(?:\.\/|\.\.\/)*)?(?:assets|html|images?|img|css|js|fonts?|media|uploads?)[\/\w\-\.]+)(["\'])/i',
+ function ($matches) use ($baseUrl) {
+ $path = $matches[2];
+
+ // Skip if already absolute URL (http://, https://, //, data:, mailto:, etc.)
+ if (preg_match('/^(https?:|\/\/|data:|mailto:|#|javascript:)/i', $path)) {
+ return $matches[0];
+ }
+
+ // Remove leading slash if exists (we'll add it via asset helper)
+ $path = ltrim($path, '/');
+
+ // Convert to absolute URL using Laravel's asset helper
+ $absoluteUrl = asset($path);
+
+ return $matches[1] . $absoluteUrl . $matches[3];
+ },
+ $html
+ );
+
+ // Pattern 2: Match CSS url() functions with relative paths
+ // Matches: url('assets/...'), url("assets/..."), url(assets/...)
+ $html = preg_replace_callback(
+ '/url\s*\(\s*(["\']?)((?:(?:\.\/|\.\.\/)*)?(?:assets|html|images?|img|css|js|fonts?|media|uploads?)[\/\w\-\.]+)\1\s*\)/i',
+ function ($matches) use ($baseUrl) {
+ $path = $matches[2];
+
+ // Skip if already absolute URL
+ if (preg_match('/^(https?:|\/\/|data:)/i', $path)) {
+ return $matches[0];
+ }
+
+ // Remove leading slash
+ $path = ltrim($path, '/');
+
+ // Convert to absolute URL
+ $absoluteUrl = asset($path);
+
+ return 'url(' . $matches[1] . $absoluteUrl . $matches[1] . ')';
+ },
+ $html
+ );
+
+ // Pattern 3: Match inline style attributes with background-image or background
+ // This is already covered by Pattern 2 (url()), but we ensure it works
+ $html = preg_replace_callback(
+ '/(style\s*=\s*["\'])([^"\']*)(["\'])/i',
+ function ($matches) {
+ // Process url() in style attribute
+ return $matches[1] . preg_replace_callback(
+ '/url\s*\(\s*(["\']?)((?:(?:\.\/|\.\.\/)*)?(?:assets|html|images?|img|css|js|fonts?|media|uploads?)[\/\w\-\.]+)\1\s*\)/i',
+ function ($urlMatches) {
+ $path = $urlMatches[2];
+ if (preg_match('/^(https?:|\/\/|data:)/i', $path)) {
+ return $urlMatches[0];
+ }
+ $path = ltrim($path, '/');
+ $absoluteUrl = asset($path);
+ return 'url(' . $urlMatches[1] . $absoluteUrl . $urlMatches[1] . ')';
+ },
+ $matches[2]
+ ) . $matches[3];
+ },
+ $html
+ );
+
+ return $html;
+ }
+
+ /**
+ * Get full HTML structure for preview
+ *
+ * @param string $content The template HTML content
+ * @param string $type The template type: 'header', 'footer', or 'section'
+ * @return string
+ */
+ public static function getPreviewHtml(string $content, string $type = 'section'): string
+ {
+ $cssLinks = self::getCssLinks();
+ $jsScripts = self::getJsScripts();
+ $meta = config('template-preview.meta', []);
+
+ $viewport = $meta['viewport'] ?? 'width=device-width, initial-scale=1';
+ $charset = $meta['charset'] ?? 'utf-8';
+ $baseUrl = rtrim(url('/'), '/');
+
+ // Convert relative paths to absolute URLs in content
+ $content = self::convertRelativePathsToAbsolute($content);
+
+ // Wrap content based on type
+ $bodyContent = match($type) {
+ 'header' => $content,
+ 'footer' => $content,
+ 'menu' => $content,
+ 'section' => '' . $content . '
',
+ default => $content,
+ };
+
+ return <<
+
+
+
+
+ Template Preview
+
+ {$cssLinks}
+
+
+ {$bodyContent}
+ {$jsScripts}
+
+
+HTML;
+ }
+}
+
diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php
new file mode 100644
index 0000000..ee82bec
--- /dev/null
+++ b/app/Services/TemplateService.php
@@ -0,0 +1,450 @@
+html_content);
+ $fields = [];
+
+ foreach ($placeholders as $placeholder) {
+ $parts = explode('.', $placeholder);
+
+ if (count($parts) !== 2) {
+ continue; // Skip invalid placeholders
+ }
+
+ [$type, $name] = $parts;
+
+ // Skip custom.* placeholders - they are blade components, not form fields
+ if ($type === 'custom') {
+ continue;
+ }
+
+ $label = str($name)->title()->replace('_', ' ')->toString();
+
+ // Get existing value from existingData if provided
+ $existingValue = $existingData[$placeholder] ?? null;
+
+ // Get default value from template's default_data
+ $defaultValue = $defaultData[$placeholder] ?? null;
+
+ // If existing value is empty (null, empty string, empty array), use default
+ $isValueEmpty = $existingValue === null
+ || $existingValue === ''
+ || (is_array($existingValue) && empty($existingValue));
+
+ // Use default if existing value is empty
+ $finalValue = $isValueEmpty && $defaultValue !== null ? $defaultValue : $existingValue;
+
+ $field = match($type) {
+ // Text Input Variants
+ 'text' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->maxLength(255),
+
+ 'email' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->email()
+ ->maxLength(255),
+
+ 'url' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->url()
+ ->maxLength(500),
+
+ 'tel' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->tel()
+ ->maxLength(20),
+
+ 'number' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->numeric(),
+
+ 'password' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->password()
+ ->dehydrated(fn ($state) => filled($state))
+ ->maxLength(255),
+
+ // Text Area & Editors
+ 'textarea' => Textarea::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->rows(4)
+ ->maxLength(5000),
+
+ 'richtext' => RichEditor::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->toolbarButtons([
+ 'bold', 'italic', 'underline', 'link',
+ 'bulletList', 'orderedList', 'h2', 'h3',
+ ])
+ ->maxLength(50000),
+
+ 'markdown' => MarkdownEditor::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->toolbarButtons([
+ 'bold', 'italic', 'strike', 'link',
+ 'heading', 'bulletList', 'orderedList', 'codeBlock',
+ ])
+ ->maxLength(50000),
+
+ 'code' => CodeEditor::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ //->lineNumbers()
+ ->maxLength(50000),
+
+ // Date & Time
+ 'date' => DatePicker::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->native(false),
+
+ 'datetime' => DateTimePicker::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->native(false)
+ ->seconds(false),
+
+ 'time' => TimePicker::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->native(false)
+ ->seconds(false),
+
+ // File Uploads
+ 'image' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->image()
+ ->disk('public')
+ ->directory('templates/images')
+ ->imageEditor()
+ ->imageEditorAspectRatios([null, '16:9', '4:3', '1:1'])
+ ->maxSize(5120),
+
+ 'images' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->image()
+ ->disk('public')
+ ->directory('templates/images')
+ ->multiple()
+ ->imageEditor()
+ ->maxSize(5120)
+ ->maxFiles(10),
+
+ 'file' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->disk('public')
+ ->directory('templates/files')
+ ->maxSize(10240),
+
+ 'files' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->disk('public')
+ ->directory('templates/files')
+ ->multiple()
+ ->maxSize(10240)
+ ->maxFiles(10),
+
+ // Selection & Options
+ 'select' => Select::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->searchable(),
+
+ 'multiselect' => Select::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->multiple()
+ ->options(self::getSelectOptions($name))
+ ->searchable(),
+
+ 'checkbox' => Checkbox::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->inline(false),
+
+ 'checkboxlist' => CheckboxList::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->columns(2),
+
+ 'radio' => Radio::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->inline()
+ ->inlineLabel(false),
+
+ 'toggle' => Toggle::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->inline(false),
+
+ 'togglebuttons' => ToggleButtons::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->inline()
+ ->grouped(),
+
+ // Color & Visual
+ 'color' => ColorPicker::make("{$dataKey}.{$placeholder}")
+ ->label($label),
+
+ // Structured Data
+ 'tags' => TagsInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->separator(','),
+
+ 'keyvalue' => KeyValue::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->keyLabel('Key')
+ ->valueLabel('Value')
+ ->reorderable(),
+
+ // Advanced
+ 'slider' => Slider::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->minValue(0)
+ ->maxValue(100)
+ ->step(1),
+
+ 'hidden' => Hidden::make("{$dataKey}.{$placeholder}"),
+
+ // Default
+ default => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->maxLength(255)
+ ->helperText("Unknown type: {$type}"),
+ };
+
+ // Set default value: use finalValue (which falls back to default if existing is empty)
+ if ($finalValue !== null) {
+ $field->default($finalValue);
+ }
+
+ $fields[] = $field;
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Parse placeholders from HTML content
+ * Format: {type.field_name} or {type.field-name}
+ */
+ public static function parsePlaceholders(string $html): array
+ {
+ preg_match_all('/\{([a-z]+\.[a-z][a-z_-]*)\}/i', $html, $matches);
+ return array_unique($matches[1] ?? []);
+ }
+
+ /**
+ * Get select options for a field
+ * Can be extended to load from database or config
+ */
+ protected static function getSelectOptions(string $fieldName): array
+ {
+ return config("template-options.{$fieldName}", [
+ 'option_1' => __('Option 1'),
+ 'option_2' => __('Option 2'),
+ 'option_3' => __('Option 3'),
+ ]);
+ }
+
+ /**
+ * Replace placeholders in HTML with actual data
+ * Supports both {text.title} and {{text.title}} formats
+ * Also supports {menu} and {staticMenu} placeholders for menu rendering
+ * Also supports {custom.component_name} for custom blade components
+ */
+ public static function replacePlaceholders(string $html, array $data): string
+ {
+ // Handle special {menu} placeholder first
+ if (str_contains($html, '{menu}')) {
+ $renderedMenu = \App\Services\MenuService::render();
+ $html = str_replace('{menu}', $renderedMenu, $html);
+ }
+
+ // Handle special {staticMenu} placeholder
+ if (str_contains($html, '{staticMenu}')) {
+ $renderedStaticMenu = view('components.static-menu')->render();
+ $html = str_replace('{staticMenu}', $renderedStaticMenu, $html);
+ }
+
+ // Handle custom blade components: {custom.component_name} or {custom.component-name}
+ preg_match_all('/\{custom\.([a-z][a-z_-]*)\}/i', $html, $customMatches);
+ if (!empty($customMatches[0])) {
+ foreach ($customMatches[0] as $index => $fullMatch) {
+ $componentName = $customMatches[1][$index] ?? null;
+ if ($componentName) {
+ // Normalize component name to lowercase for consistent file system access
+ $componentName = strtolower($componentName);
+ $viewPath = "components.custom.{$componentName}";
+ if (view()->exists($viewPath)) {
+ try {
+ $renderedComponent = view($viewPath)->render();
+ // Ensure rendered component is properly formatted
+ $renderedComponent = trim($renderedComponent);
+ $html = str_replace($fullMatch, $renderedComponent, $html);
+ } catch (\Exception $e) {
+ // Log the error for debugging
+ \Log::error("Custom component render error: {$viewPath}", [
+ 'error' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]);
+
+ // Show error message in preview (visible in development)
+ $errorMessage = htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
+ $errorHtml = "" .
+ "Custom Component Error: {$viewPath}
" .
+ "{$errorMessage}" .
+ "
";
+ $html = str_replace($fullMatch, $errorHtml, $html);
+ }
+ } else {
+ // Component doesn't exist - show notice in preview
+ $noticeHtml = "" .
+ "Custom Component Not Found: {$viewPath}
" .
+ "Create the file at: resources/views/components/custom/{$componentName}.blade.php" .
+ "
";
+ $html = str_replace($fullMatch, $noticeHtml, $html);
+ }
+ }
+ }
+ }
+
+ // First, parse all placeholders in the format {type.field_name} or {type.field-name}
+ // Exclude custom.* from this regex to avoid double processing
+ preg_match_all('/\{([a-z]+\.[a-z][a-z_-]*)\}/i', $html, $matches);
+ $placeholders = array_unique($matches[1] ?? []);
+
+ // Filter out custom.* placeholders as they're already handled above
+ $placeholders = array_filter($placeholders, function($placeholder) {
+ return !str_starts_with($placeholder, 'custom.');
+ });
+
+ // Replace each placeholder
+ foreach ($placeholders as $placeholder) {
+ // Try to find the value in data with various key formats
+ $value = null;
+
+ // 1. Try exact match (text.title)
+ if (isset($data[$placeholder])) {
+ $value = $data[$placeholder];
+ }
+ // 2. Try with section_data prefix (section_data.text.title)
+ elseif (isset($data["section_data.{$placeholder}"])) {
+ $value = $data["section_data.{$placeholder}"];
+ }
+ // 3. Try nested array access (data['section_data']['text.title'] or data['text']['title'])
+ else {
+ $parts = explode('.', $placeholder);
+ if (count($parts) === 2) {
+ [$type, $field] = $parts;
+ // Try nested: data['section_data'][$type][$field]
+ if (isset($data['section_data'][$type][$field])) {
+ $value = $data['section_data'][$type][$field];
+ }
+ // Try nested: data[$type][$field]
+ elseif (isset($data[$type][$field])) {
+ $value = $data[$type][$field];
+ }
+ }
+ }
+
+ // Extract type from placeholder to determine if it's a file/image
+ $parts = explode('.', $placeholder);
+ $type = $parts[0] ?? null;
+
+ // Handle different value types
+ if (is_array($value)) {
+ // Handle arrays (e.g., multiple images)
+ $value = array_map(function($item) use ($type) {
+ return self::formatFileUrl($item, $type);
+ }, $value);
+ $value = implode(', ', $value);
+ } elseif (is_bool($value)) {
+ $value = $value ? 'true' : 'false';
+ } elseif ($value === null) {
+ $value = '';
+ } else {
+ // Format file/image URLs
+ $value = self::formatFileUrl($value, $type);
+ }
+
+ // Replace both {text.title} and {{text.title}} formats
+ $html = str_replace(
+ ["{{$placeholder}}", "{{{$placeholder}}}"],
+ [$value, $value],
+ $html
+ );
+ }
+
+ return $html;
+ }
+
+ /**
+ * Format file URL based on type
+ * Converts storage paths to public URLs
+ */
+ protected static function formatFileUrl($value, $type): string
+ {
+ // If it's already a full URL, return as is
+ if (is_string($value) && (str_starts_with($value, 'http://') || str_starts_with($value, 'https://'))) {
+ return $value;
+ }
+
+ // Only process image and file types
+ if (!in_array($type, ['image', 'images', 'file', 'files'])) {
+ return (string) $value;
+ }
+
+ // If value is empty, return empty string
+ if (empty($value) || !is_string($value)) {
+ return '';
+ }
+
+ // Check if the path starts with storage/ (already formatted)
+ if (str_starts_with($value, 'storage/')) {
+ return asset($value);
+ }
+
+ // Remove private/ prefix if exists (old format from private disk)
+ if (str_starts_with($value, 'private/')) {
+ $value = str_replace('private/', '', $value);
+ }
+
+ // Check if the path is a relative path that should be in storage
+ // Templates are stored in templates/images or templates/files
+ if (str_starts_with($value, 'templates/')) {
+ return asset('storage/' . $value);
+ }
+
+ // If it's already using asset() or Storage::url(), assume it's already formatted
+ // Otherwise, prepend storage/ if it looks like a storage path
+ if (!str_starts_with($value, '/')) {
+ return asset('storage/' . $value);
+ }
+
+ return $value;
+ }
+}
+
diff --git a/app/View/Components/PageRenderer.php b/app/View/Components/PageRenderer.php
new file mode 100644
index 0000000..c94b561
--- /dev/null
+++ b/app/View/Components/PageRenderer.php
@@ -0,0 +1,96 @@
+page = $page;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return view('components.page-renderer');
+ }
+
+ /**
+ * Render the header template with data
+ */
+ public function renderHeader(): string
+ {
+ if (!$this->page->headerTemplate) {
+ return '';
+ }
+
+ // Merge template defaults with page data (page data overrides defaults)
+ $templateDefaults = $this->page->headerTemplate->default_data ?? [];
+ $pageData = $this->page->header_data ?? [];
+ $mergedData = array_merge($templateDefaults, $pageData);
+
+ return TemplateService::replacePlaceholders(
+ $this->page->headerTemplate->html_content,
+ $mergedData
+ );
+ }
+
+ /**
+ * Render all sections with their templates
+ */
+ public function renderSections(): string
+ {
+ $output = '';
+
+ foreach ($this->page->templated_sections as $section) {
+ if (!$section['template']) {
+ continue;
+ }
+
+ // Merge template defaults with section data (section data overrides defaults)
+ $templateDefaults = $section['template']->default_data ?? [];
+ $sectionData = $section['data'] ?? [];
+ $mergedData = array_merge($templateDefaults, $sectionData);
+
+ $output .= TemplateService::replacePlaceholders(
+ $section['template']->html_content,
+ $mergedData
+ );
+ }
+
+ return $output;
+ }
+
+ /**
+ * Render the footer template with data
+ */
+ public function renderFooter(): string
+ {
+ if (!$this->page->footerTemplate) {
+ return '';
+ }
+
+ // Merge template defaults with page data (page data overrides defaults)
+ $templateDefaults = $this->page->footerTemplate->default_data ?? [];
+ $pageData = $this->page->footer_data ?? [];
+ $mergedData = array_merge($templateDefaults, $pageData);
+
+ return TemplateService::replacePlaceholders(
+ $this->page->footerTemplate->html_content,
+ $mergedData
+ );
+ }
+}
diff --git a/bootstrap/app.php b/bootstrap/app.php
index c183276..ca1d20c 100644
--- a/bootstrap/app.php
+++ b/bootstrap/app.php
@@ -11,7 +11,9 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
- //
+ $middleware->web(append: [
+ \App\Http\Middleware\SetLocale::class,
+ ]);
})
->withExceptions(function (Exceptions $exceptions): void {
//
diff --git a/composer.json b/composer.json
index b3ff955..9564a87 100644
--- a/composer.json
+++ b/composer.json
@@ -11,6 +11,7 @@
"filament/filament": "~4.0",
"laravel/framework": "^12.0",
"laravel/tinker": "^2.10.1",
+ "solution-forest/filament-tree": "^3.0",
"spatie/laravel-permission": "^6.21",
"tomatophp/filament-users": "^4.0"
},
diff --git a/composer.lock b/composer.lock
index d7709cf..3da6d38 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "ae5eda2a09c03c50635001cd6005dbec",
+ "content-hash": "131798d367c222bf82f94782fbafa942",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -5395,6 +5395,77 @@
],
"time": "2022-12-17T21:53:22+00:00"
},
+ {
+ "name": "solution-forest/filament-tree",
+ "version": "3.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/solutionforest/filament-tree.git",
+ "reference": "2722a10d782585c6063ec50903eb53f186e5202f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/solutionforest/filament-tree/zipball/2722a10d782585c6063ec50903eb53f186e5202f",
+ "reference": "2722a10d782585c6063ec50903eb53f186e5202f",
+ "shasum": ""
+ },
+ "require": {
+ "filament/filament": "^4.0",
+ "php": "^8.1",
+ "spatie/laravel-package-tools": "^1.15.0"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.0",
+ "laravel/pint": "^1.0",
+ "nunomaduro/collision": "^8.0",
+ "orchestra/testbench": "^9.0|^10.0",
+ "pestphp/pest": "^3.0",
+ "pestphp/pest-plugin-arch": "^3.0",
+ "pestphp/pest-plugin-laravel": "^3.0",
+ "pestphp/pest-plugin-livewire": "^3.0",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "rector/rector": "^2.0",
+ "spatie/laravel-ray": "^1.26"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "SolutionForest\\FilamentTree\\FilamentTreeServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "SolutionForest\\FilamentTree\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Carly",
+ "email": "info@solutionforest.net",
+ "role": "Developer"
+ }
+ ],
+ "description": "This is a tree layout plugin for Filament Admin",
+ "homepage": "https://github.com/solution-forest/filament-tree",
+ "keywords": [
+ "Solution Forest",
+ "filament-tree",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/solution-forest/filament-tree/issues",
+ "source": "https://github.com/solution-forest/filament-tree"
+ },
+ "time": "2025-10-22T03:25:18+00:00"
+ },
{
"name": "spatie/invade",
"version": "2.1.0",
diff --git a/config/template-options.php b/config/template-options.php
new file mode 100644
index 0000000..f3b8095
--- /dev/null
+++ b/config/template-options.php
@@ -0,0 +1,43 @@
+ ['key' => 'Label']
+ |
+ */
+
+ // Example options - Add your own as needed
+ 'example_select' => [
+ 'option_1' => 'Option 1',
+ 'option_2' => 'Option 2',
+ 'option_3' => 'Option 3',
+ ],
+
+ 'status' => [
+ 'active' => 'Active',
+ 'inactive' => 'Inactive',
+ 'pending' => 'Pending',
+ ],
+
+ 'size' => [
+ 'small' => 'Small',
+ 'medium' => 'Medium',
+ 'large' => 'Large',
+ ],
+
+ 'color_scheme' => [
+ 'light' => 'Light',
+ 'dark' => 'Dark',
+ 'auto' => 'Auto',
+ ],
+
+ // Add more options as needed
+];
+
diff --git a/config/template-preview.php b/config/template-preview.php
new file mode 100644
index 0000000..73c5b2c
--- /dev/null
+++ b/config/template-preview.php
@@ -0,0 +1,54 @@
+ [
+ 'style' => 'html/style.css',
+ 'unicons' => 'assets/fonts/unicons/unicons.css',
+ 'plugins' => 'assets/css/plugins.css',
+ 'colors' => 'assets/css/colors/grape.css',
+ 'fonts' => 'assets/css/fonts/urbanist.css',
+ ],
+
+ // JavaScript Assets
+ 'js' => [
+ 'plugins' => 'assets/js/plugins.js',
+ 'theme' => 'assets/js/theme.js',
+ ],
+
+ // Additional meta tags and head content
+ 'meta' => [
+ 'viewport' => 'width=device-width, initial-scale=1',
+ 'charset' => 'utf-8',
+ ],
+
+ // Whether to load assets from Settings table (overrides config)
+ 'use_settings_override' => false,
+
+ // Settings keys for asset paths (if use_settings_override is true)
+ 'settings_keys' => [
+ 'css_style' => 'template_preview_css_style',
+ 'css_unicons' => 'template_preview_css_unicons',
+ 'css_plugins' => 'template_preview_css_plugins',
+ 'css_colors' => 'template_preview_css_colors',
+ 'css_fonts' => 'template_preview_css_fonts',
+ 'js_plugins' => 'template_preview_js_plugins',
+ 'js_theme' => 'template_preview_js_theme',
+ ],
+];
+
diff --git a/database/migrations/2025_10_30_184739_add_sections_to_pages_table.php b/database/migrations/2025_10_30_184739_add_sections_to_pages_table.php
new file mode 100644
index 0000000..07ee766
--- /dev/null
+++ b/database/migrations/2025_10_30_184739_add_sections_to_pages_table.php
@@ -0,0 +1,29 @@
+json('sections')->nullable()->after('content');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('pages', function (Blueprint $table) {
+ $table->dropColumn('sections');
+ });
+ }
+};
diff --git a/database/migrations/2025_10_31_173856_create_header_templates_table.php b/database/migrations/2025_10_31_173856_create_header_templates_table.php
new file mode 100644
index 0000000..b5ba3f6
--- /dev/null
+++ b/database/migrations/2025_10_31_173856_create_header_templates_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('title');
+ $table->longText('html_content');
+ $table->boolean('is_active')->default(true);
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('header_templates');
+ }
+};
diff --git a/database/migrations/2025_10_31_173857_create_section_templates_table.php b/database/migrations/2025_10_31_173857_create_section_templates_table.php
new file mode 100644
index 0000000..58f7703
--- /dev/null
+++ b/database/migrations/2025_10_31_173857_create_section_templates_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('title');
+ $table->longText('html_content');
+ $table->boolean('is_active')->default(true);
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('section_templates');
+ }
+};
diff --git a/database/migrations/2025_10_31_173858_create_footer_templates_table.php b/database/migrations/2025_10_31_173858_create_footer_templates_table.php
new file mode 100644
index 0000000..be8e7d2
--- /dev/null
+++ b/database/migrations/2025_10_31_173858_create_footer_templates_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('title');
+ $table->longText('html_content');
+ $table->boolean('is_active')->default(true);
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('footer_templates');
+ }
+};
diff --git a/database/migrations/2025_10_31_173859_add_template_fields_to_pages_table.php b/database/migrations/2025_10_31_173859_add_template_fields_to_pages_table.php
new file mode 100644
index 0000000..ea9b394
--- /dev/null
+++ b/database/migrations/2025_10_31_173859_add_template_fields_to_pages_table.php
@@ -0,0 +1,42 @@
+foreignId('header_template_id')->nullable()->after('id')->constrained('header_templates')->nullOnDelete();
+ $table->json('header_data')->nullable()->after('header_template_id');
+
+ $table->foreignId('footer_template_id')->nullable()->after('header_data')->constrained('footer_templates')->nullOnDelete();
+ $table->json('footer_data')->nullable()->after('footer_template_id');
+
+ $table->json('sections_data')->nullable()->after('footer_data');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('pages', function (Blueprint $table) {
+ $table->dropForeign(['header_template_id']);
+ $table->dropColumn('header_template_id');
+ $table->dropColumn('header_data');
+
+ $table->dropForeign(['footer_template_id']);
+ $table->dropColumn('footer_template_id');
+ $table->dropColumn('footer_data');
+
+ $table->dropColumn('sections_data');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_01_045504_add_default_data_to_templates_table.php b/database/migrations/2025_11_01_045504_add_default_data_to_templates_table.php
new file mode 100644
index 0000000..7560a42
--- /dev/null
+++ b/database/migrations/2025_11_01_045504_add_default_data_to_templates_table.php
@@ -0,0 +1,44 @@
+json('default_data')->nullable()->after('html_content');
+ });
+
+ Schema::table('footer_templates', function (Blueprint $table) {
+ $table->json('default_data')->nullable()->after('html_content');
+ });
+
+ Schema::table('section_templates', function (Blueprint $table) {
+ $table->json('default_data')->nullable()->after('html_content');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('header_templates', function (Blueprint $table) {
+ $table->dropColumn('default_data');
+ });
+
+ Schema::table('footer_templates', function (Blueprint $table) {
+ $table->dropColumn('default_data');
+ });
+
+ Schema::table('section_templates', function (Blueprint $table) {
+ $table->dropColumn('default_data');
+ });
+ }
+};
diff --git a/database/migrations/2025_11_03_183415_create_menu_templates_table.php b/database/migrations/2025_11_03_183415_create_menu_templates_table.php
new file mode 100644
index 0000000..ec6bec0
--- /dev/null
+++ b/database/migrations/2025_11_03_183415_create_menu_templates_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('title');
+ $table->longText('html_content');
+ $table->boolean('is_active')->default(true);
+ $table->timestamps();
+ $table->softDeletes();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('menu_templates');
+ }
+};
diff --git a/database/seeders/PageSeeder.php b/database/seeders/PageSeeder.php
new file mode 100644
index 0000000..ab5e546
--- /dev/null
+++ b/database/seeders/PageSeeder.php
@@ -0,0 +1,316 @@
+ 'Admin',
+ 'email' => 'admin@truncgil.com',
+ 'password' => bcrypt('password'),
+ ]);
+ }
+
+ $pages = [
+ // Home Page
+ [
+ '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' => 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ı Web Çözümleri',
+ '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 fark yaratın',
+ '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
+ [
+ 'author_id' => $author->id,
+ 'title' => 'Hakkımızda',
+ 'slug' => 'about',
+ 'excerpt' => 'Truncgil Citrus olarak yenilikçi teknoloji çözümleri sunuyoruz',
+ 'content' => null,
+ 'template' => 'generic',
+ 'status' => 'published',
+ 'is_homepage' => false,
+ '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 Çözümleri',
+ '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
+ [
+ 'author_id' => $author->id,
+ 'title' => 'Hizmetlerimiz',
+ 'slug' => 'services',
+ 'excerpt' => 'Geniş yelpazede teknoloji hizmetleri sunuyoruz',
+ 'content' => null,
+ '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 Çözümleri',
+ '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
+ [
+ 'author_id' => $author->id,
+ 'title' => 'İletişim',
+ 'slug' => 'contact',
+ 'excerpt' => 'Projeleriniz hakkında bizimle iletişime geçin',
+ 'content' => null,
+ 'template' => 'generic',
+ '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 Konuşalım',
+ '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)
+ [
+ 'author_id' => $author->id,
+ 'title' => 'Fiyatlandırma',
+ 'slug' => 'pricing',
+ 'excerpt' => 'Size uygun paketi seçin',
+ 'content' => null,
+ 'template' => 'generic',
+ 'status' => 'published',
+ 'is_homepage' => false,
+ 'show_in_menu' => false,
+ '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 Paketi Seçin',
+ '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
+ );
+ }
+
+ $this->command->info('✅ ' . count($pages) . ' sayfa başarıyla oluşturuldu!');
+ }
+}
diff --git a/database/seeders/SettingSeeder.php b/database/seeders/SettingSeeder.php
index 487976c..6f15bea 100644
--- a/database/seeders/SettingSeeder.php
+++ b/database/seeders/SettingSeeder.php
@@ -525,10 +525,10 @@ class SettingSeeder extends Seeder
[
'key' => 'social_links',
'value' => json_encode([
- 'facebook' => '',
- 'twitter' => '',
- 'instagram' => '',
- 'linkedin' => '',
+ 'facebook' => 'https://facebook.com/',
+ 'twitter' => 'https://twitter.com/',
+ 'instagram' => 'https://instagram.com/',
+ 'linkedin' => 'https://linkedin.com/',
'youtube' => '',
'tiktok' => '',
'github' => '',
@@ -541,6 +541,60 @@ class SettingSeeder extends Seeder
'is_public' => true,
'is_active' => true,
],
+
+ // ==========================================
+ // FRONTEND LAYOUT AYARLARI (layout)
+ // ==========================================
+ [
+ 'key' => 'default_meta_title',
+ 'value' => 'Truncgil Citrus - Modern Web Çözümleri',
+ 'type' => 'string',
+ 'group' => 'layout',
+ 'label' => 'Varsayılan Meta Başlık',
+ 'description' => 'Sayfalarda özel meta başlık yoksa kullanılacak',
+ 'is_public' => true,
+ 'is_active' => true,
+ ],
+ [
+ 'key' => 'default_meta_description',
+ 'value' => 'Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz',
+ 'type' => 'text',
+ 'group' => 'layout',
+ 'label' => 'Varsayılan Meta Açıklama',
+ 'description' => 'Sayfalarda özel meta açıklama yoksa kullanılacak',
+ 'is_public' => true,
+ 'is_active' => true,
+ ],
+ [
+ 'key' => 'default_meta_image',
+ 'value' => 'assets/img/demos/f1.png',
+ 'type' => 'string',
+ 'group' => 'layout',
+ 'label' => 'Varsayılan Meta Görsel',
+ 'description' => 'Open Graph için varsayılan görsel',
+ 'is_public' => true,
+ 'is_active' => true,
+ ],
+ [
+ 'key' => 'logo_path',
+ 'value' => 'assets/img/truncgil-yatay.svg',
+ 'type' => 'string',
+ 'group' => 'layout',
+ 'label' => 'Logo Yolu',
+ 'description' => 'Header\'da görünecek logo',
+ 'is_public' => true,
+ 'is_active' => true,
+ ],
+ [
+ 'key' => 'favicon_path',
+ 'value' => 'assets/img/favicon.ico',
+ 'type' => 'string',
+ 'group' => 'layout',
+ 'label' => 'Favicon Yolu',
+ 'description' => 'Tarayıcı favicon',
+ 'is_public' => true,
+ 'is_active' => true,
+ ],
// ==========================================
// SEO AYARLARI (seo)
diff --git a/docs/BLOCKS_USAGE.md b/docs/BLOCKS_USAGE.md
new file mode 100644
index 0000000..67d07b5
--- /dev/null
+++ b/docs/BLOCKS_USAGE.md
@@ -0,0 +1,424 @@
+# Blok Tabanlı Sayfa Oluşturma Sistemi
+
+## Genel Bakış
+
+Sistemde 9 adet hazır blok bileşeni bulunmaktadır. Bu bloklar admin panelinden seçilip veri girilerek sayfa oluşturulabilir.
+
+## Mevcut Bloklar
+
+### 1. Hero (Ana Başlık)
+**Kullanım:** Sayfa başlığı, açıklama, butonlar ve görsel
+**Dosya:** `resources/views/components/blocks/hero.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "hero",
+ "data": {
+ "background_image": "assets/img/photos/blurry.png",
+ "badge": "YENİ SÜRÜM",
+ "title": "Modern ve Çok Amaçlı Web Çözümleri",
+ "subtitle": "Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz",
+ "primary_button_text": "Hemen Başla",
+ "primary_button_url": "#",
+ "secondary_button_text": "Daha Fazla Bilgi",
+ "secondary_button_url": "#about",
+ "hero_image": "assets/img/demos/f1.png"
+ }
+}
+```
+
+---
+
+### 2. Features (Özellikler Grid)
+**Kullanım:** İkon + başlık + açıklama kartları
+**Dosya:** `resources/views/components/blocks/features.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "features",
+ "data": {
+ "bg_class": "!bg-[#f0f0f8]",
+ "section_badge": "ÖZELLİKLER",
+ "section_title": "Neden Bizi Tercih Etmelisiniz?",
+ "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/icons/fast.svg",
+ "title": "Hızlı Çözümler",
+ "description": "Modern teknolojiler kullanarak projelerinizi hızlı ve verimli bir şekilde hayata geçiriyoruz."
+ },
+ {
+ "icon": "assets/img/icons/secure.svg",
+ "title": "Güvenli Altyapı",
+ "description": "En son güvenlik standartlarını kullanarak verilerinizi koruma altına alıyoruz."
+ },
+ {
+ "icon": "assets/img/icons/team.svg",
+ "title": "Uzman Ekip",
+ "description": "Deneyimli ve uzman ekibimiz ile her zaman yanınızdayız."
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 3. Stats (İstatistikler/Sayaçlar)
+**Kullanım:** Sayı + açıklama istatistikleri
+**Dosya:** `resources/views/components/blocks/stats.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "stats",
+ "data": {
+ "bg_class": "!bg-[#ffffff]",
+ "column_class": "md:w-6/12 lg:w-3/12",
+ "stats": [
+ {
+ "icon": "assets/img/icons/project.svg",
+ "number": "500+",
+ "label": "Tamamlanan Proje"
+ },
+ {
+ "icon": "assets/img/icons/client.svg",
+ "number": "300+",
+ "label": "Mutlu Müşteri"
+ },
+ {
+ "icon": "assets/img/icons/award.svg",
+ "number": "50+",
+ "label": "Kazanılan Ödül"
+ },
+ {
+ "icon": "assets/img/icons/team-stat.svg",
+ "number": "25+",
+ "label": "Ekip Üyesi"
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 4. Testimonials (Referanslar/Yorumlar)
+**Kullanım:** Müşteri yorumları
+**Dosya:** `resources/views/components/blocks/testimonials.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "testimonials",
+ "data": {
+ "bg_class": "!bg-[#f0f0f8]",
+ "section_badge": "REFERANSLAR",
+ "section_title": "Müşterilerimiz Ne Diyor?",
+ "section_subtitle": "Binlerce mutlu müşterimizden bazılarının görüşleri",
+ "column_class": "md:w-6/12 lg:w-4/12",
+ "testimonials": [
+ {
+ "rating": 5,
+ "quote": "Harika bir ekip! Projemizi zamanında ve beklentilerimizin üzerinde teslim ettiler.",
+ "avatar": "assets/img/avatars/te1.jpg",
+ "name": "Ahmet Yılmaz",
+ "position": "CEO, ABC Tech"
+ },
+ {
+ "rating": 5,
+ "quote": "Profesyonel yaklaşım ve mükemmel sonuç. Kesinlikle tavsiye ediyorum!",
+ "avatar": "assets/img/avatars/te2.jpg",
+ "name": "Ayşe Demir",
+ "position": "Kurucu, XYZ Startup"
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 5. Pricing (Fiyatlandırma Tabloları)
+**Kullanım:** Fiyat paketleri
+**Dosya:** `resources/views/components/blocks/pricing.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "pricing",
+ "data": {
+ "bg_class": "!bg-[#ffffff]",
+ "section_badge": "FİYATLANDIRMA",
+ "section_title": "Size Uygun Paketi Seçin",
+ "section_subtitle": "Tüm paketler 30 gün para iade garantisi ile geliyor",
+ "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": "#"
+ },
+ {
+ "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": "#"
+ },
+ {
+ "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": "#"
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 6. CTA (Call To Action)
+**Kullanım:** Dikkat çekici aksiyon çağrısı
+**Dosya:** `resources/views/components/blocks/cta.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "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 fark yaratın",
+ "subtitle": "Binlerce müşterimiz tarafından güveniliyoruz. Siz de katılın ve kısa sürede çarpıcı web sitenizi kolayca oluşturun.",
+ "button_text": "Hemen Başla",
+ "button_url": "#",
+ "button_icon": "uil uil-arrow-up-right",
+ "cta_image": "assets/img/demos/f1.png"
+ }
+}
+```
+
+---
+
+### 7. Gallery (Galeri/Portfolio)
+**Kullanım:** Proje galerisi veya portfolio
+**Dosya:** `resources/views/components/blocks/gallery.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "gallery",
+ "data": {
+ "bg_class": "!bg-[#f0f0f8]",
+ "section_badge": "PORTFOLİO",
+ "section_title": "Projelerimiz",
+ "section_subtitle": "Son dönemde tamamladığımız bazı projeler",
+ "column_class": "md:w-6/12 lg:w-4/12",
+ "items": [
+ {
+ "image": "assets/img/portfolio/p1.jpg",
+ "category": "Web Tasarım",
+ "title": "E-Ticaret Sitesi",
+ "description": "Modern ve kullanıcı dostu e-ticaret platformu",
+ "link": "#"
+ },
+ {
+ "image": "assets/img/portfolio/p2.jpg",
+ "category": "Mobil Uygulama",
+ "title": "Fitness Uygulaması",
+ "description": "iOS ve Android için fitness takip uygulaması",
+ "link": "#"
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 8. Team (Ekip Üyeleri)
+**Kullanım:** Ekip üyeleri tanıtımı
+**Dosya:** `resources/views/components/blocks/team.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "team",
+ "data": {
+ "bg_class": "!bg-[#ffffff]",
+ "section_badge": "EKİBİMİZ",
+ "section_title": "Arkasındaki İnsanlar",
+ "section_subtitle": "Profesyonel ve deneyimli ekibimizle tanışın",
+ "column_class": "md:w-6/12 lg:w-3/12",
+ "members": [
+ {
+ "photo": "assets/img/team/t1.jpg",
+ "name": "Mehmet Kaya",
+ "position": "CEO & Kurucu",
+ "bio": "15 yıllık teknoloji tecrübesi",
+ "social": {
+ "twitter": "https://twitter.com/",
+ "linkedin": "https://linkedin.com/",
+ "facebook": "https://facebook.com/"
+ }
+ },
+ {
+ "photo": "assets/img/team/t2.jpg",
+ "name": "Zeynep Şahin",
+ "position": "CTO",
+ "bio": "Yazılım mimarisi uzmanı",
+ "social": {
+ "twitter": "https://twitter.com/",
+ "linkedin": "https://linkedin.com/"
+ }
+ }
+ ]
+ }
+}
+```
+
+---
+
+### 9. Contact (İletişim Formu)
+**Kullanım:** İletişim formu ve bilgileri
+**Dosya:** `resources/views/components/blocks/contact.blade.php`
+
+**Örnek Veri:**
+```json
+{
+ "type": "contact",
+ "data": {
+ "bg_class": "!bg-[#f0f0f8]",
+ "section_badge": "İLETİŞİM",
+ "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"
+ }
+}
+```
+
+---
+
+## Admin Panelinde Kullanım
+
+### Page Modelinde `sections` Alanı
+
+Admin panelinde sayfa oluştururken `sections` JSON alanına yukarıdaki blokları ekleyebilirsiniz:
+
+```json
+[
+ {
+ "type": "hero",
+ "data": { ... }
+ },
+ {
+ "type": "features",
+ "data": { ... }
+ },
+ {
+ "type": "pricing",
+ "data": { ... }
+ },
+ {
+ "type": "cta",
+ "data": { ... }
+ }
+]
+```
+
+### Filament Resource'da Repeater Kullanımı
+
+```php
+Repeater::make('sections')
+ ->schema([
+ Select::make('type')
+ ->options([
+ 'hero' => 'Hero (Ana Başlık)',
+ 'features' => 'Features (Özellikler)',
+ 'stats' => 'Stats (İstatistikler)',
+ 'testimonials' => 'Testimonials (Yorumlar)',
+ 'pricing' => 'Pricing (Fiyatlandırma)',
+ 'cta' => 'CTA (Aksiyon Çağrısı)',
+ 'gallery' => 'Gallery (Galeri)',
+ 'team' => 'Team (Ekip)',
+ 'contact' => 'Contact (İletişim)',
+ ])
+ ->required()
+ ->reactive(),
+
+ KeyValue::make('data')
+ ->label('Blok Verileri')
+ ->addActionLabel('Alan Ekle')
+ ])
+ ->collapsible()
+ ->itemLabel(fn (array $state): ?string => $state['type'] ?? null)
+```
+
+---
+
+## Özelleştirme
+
+Her blok için `bg_class`, `column_class` gibi parametrelerle Tailwind CSS sınıfları özelleştirilebilir.
+
+**Örnek:**
+```json
+{
+ "bg_class": "!bg-gradient-to-br from-[#e31e24] to-[#ff6b6b]",
+ "column_class": "md:w-4/12 lg:w-3/12"
+}
+```
+
+---
+
+## Yeni Blok Ekleme
+
+1. `resources/views/components/blocks/` altında yeni blade dosyası oluşturun
+2. `@props(['data' => []])` ile veri alın
+3. HTML yapısını oluşturun
+4. Admin repeater select'ine yeni tipi ekleyin
+
+---
+
+**Son Güncelleme:** {{ now()->format('d.m.Y') }}
+
diff --git a/docs/DYNAMIC_TEMPLATE_SYSTEM.md b/docs/DYNAMIC_TEMPLATE_SYSTEM.md
new file mode 100644
index 0000000..dc15200
--- /dev/null
+++ b/docs/DYNAMIC_TEMPLATE_SYSTEM.md
@@ -0,0 +1,895 @@
+# Dynamic Template System (Dinamik Şablon Sistemi)
+
+## Genel Bakış
+
+Bu sistem, sayfalara dinamik header, section ve footer şablonları ekleyerek içerik yönetimini tamamen özelleştirilebilir hale getirir.
+
+## Mimari
+
+### 1. Template Modülleri
+
+#### 1.1 Header Template
+- **Tablo:** `header_templates`
+- **Alanlar:**
+ - `id` (bigint, PK)
+ - `title` (string, 255) - Şablon başlığı
+ - `html_content` (longtext) - HTML içerik + placeholder'lar ✅ **4GB kapasiteli**
+ - `is_active` (boolean) - Aktif/Pasif
+ - `created_at`, `updated_at`, `deleted_at`
+
+#### 1.2 Section Template
+- **Tablo:** `section_templates`
+- **Alanlar:**
+ - `id` (bigint, PK)
+ - `title` (string, 255) - Şablon başlığı
+ - `html_content` (longtext) - HTML içerik + placeholder'lar ✅ **4GB kapasiteli**
+ - `is_active` (boolean) - Aktif/Pasif
+ - `created_at`, `updated_at`, `deleted_at`
+
+#### 1.3 Footer Template
+- **Tablo:** `footer_templates`
+- **Alanlar:**
+ - `id` (bigint, PK)
+ - `title` (string, 255) - Şablon başlığı
+ - `html_content` (longtext) - HTML içerik + placeholder'lar ✅ **4GB kapasiteli**
+ - `is_active` (boolean) - Aktif/Pasif
+ - `created_at`, `updated_at`, `deleted_at`
+
+#### 1.4 Menu Template
+- **Tablo:** `menu_templates`
+- **Alanlar:**
+ - `id` (bigint, PK)
+ - `title` (string, 255) - Şablon başlığı
+ - `html_content` (longtext) - HTML içerik + {menu} placeholder ✅ **4GB kapasiteli**
+ - `is_active` (boolean) - Aktif/Pasif
+ - `created_at`, `updated_at`, `deleted_at`
+
+**Not:** `longtext` veri tipi MySQL'de **4,294,967,295 karakter** (4GB) kapasiteye sahiptir. Bu, en karmaşık HTML template'ler ve zengin içerikler için fazlasıyla yeterlidir.
+
+### 2. Placeholder Sistemi
+
+#### 2.1 Placeholder Formatı
+```
+{form_type.field_name}
+```
+
+**Özel Placeholder'lar:**
+- `{menu}` → Menü yapısını render eder (MenuTemplate için kullanılır)
+- Diğer tüm template tiplerinde (header, footer, section) normal placeholder'lar kullanılır
+
+**Desteklenen Form Tipleri:**
+
+#### Text Input Variants:
+- `text` → TextInput (basic text)
+- `email` → TextInput (email validation)
+- `url` → TextInput (URL validation)
+- `tel` → TextInput (telephone)
+- `number` → TextInput (numeric)
+- `password` → TextInput (password)
+
+#### Text Area & Editors:
+- `textarea` → Textarea (multi-line text)
+- `richtext` → RichEditor (WYSIWYG HTML editor)
+- `markdown` → MarkdownEditor (Markdown editor with preview)
+- `code` → CodeEditor (syntax highlighted code)
+
+#### Date & Time:
+- `date` → DatePicker (date only)
+- `datetime` → DateTimePicker (date + time)
+- `time` → TimePicker (time only)
+
+#### File Uploads:
+- `image` → FileUpload (image only, with preview)
+- `file` → FileUpload (any file type)
+- `files` → FileUpload (multiple files)
+- `images` → FileUpload (multiple images)
+
+#### Selection & Options:
+- `select` → Select (dropdown selection)
+- `multiselect` → Select (multiple selection)
+- `checkbox` → Checkbox (single checkbox)
+- `checkboxlist` → CheckboxList (multiple checkboxes)
+- `radio` → Radio (radio buttons)
+- `toggle` → Toggle (switch button)
+- `togglebuttons` → ToggleButtons (button group)
+
+#### Color & Visual:
+- `color` → ColorPicker (color picker)
+
+#### Structured Data:
+- `tags` → TagsInput (tag input)
+- `keyvalue` → KeyValue (key-value pairs)
+- `repeater` → Repeater (repeatable fields - not recommended in template)
+- `builder` → Builder (block builder - not recommended in template)
+
+#### Advanced:
+- `slider` → Slider (range slider)
+- `hidden` → Hidden (hidden field for calculations)
+
+**Toplam: 30+ Form Tipi Desteği**
+
+#### Form Tipi Referans Tablosu
+
+| Placeholder Type | Filament Component | Örnek Kullanım | Veri Tipi | Max Boyut |
+|-----------------|-------------------|----------------|-----------|-----------|
+| `{text.name}` | TextInput | Kısa metinler | string | 255 char |
+| `{email.address}` | TextInput (email) | E-posta adresleri | string | 255 char |
+| `{url.link}` | TextInput (url) | Web adresleri | string | 500 char |
+| `{tel.phone}` | TextInput (tel) | Telefon numaraları | string | 20 char |
+| `{number.count}` | TextInput (numeric) | Sayısal değerler | integer/float | - |
+| `{password.pass}` | TextInput (password) | Şifreler | string (hashed) | 255 char |
+| `{textarea.desc}` | Textarea | Çok satırlı metin | string | 5000 char |
+| `{richtext.content}` | RichEditor | HTML içerik | longtext | 50000 char |
+| `{markdown.post}` | MarkdownEditor | Markdown içerik | longtext | 50000 char |
+| `{code.snippet}` | CodeEditor | Kod blokları | longtext | 50000 char |
+| `{date.birthday}` | DatePicker | Tarih seçimi | date | - |
+| `{datetime.event}` | DateTimePicker | Tarih + saat | datetime | - |
+| `{time.opening}` | TimePicker | Saat seçimi | time | - |
+| `{image.banner}` | FileUpload | Tek resim | string (path) | 5MB |
+| `{images.gallery}` | FileUpload (multiple) | Çoklu resim | json (paths) | 10x5MB |
+| `{file.document}` | FileUpload | Tek dosya | string (path) | 10MB |
+| `{files.attachments}` | FileUpload (multiple) | Çoklu dosya | json (paths) | 10x10MB |
+| `{select.category}` | Select | Tekli seçim | string/int | - |
+| `{multiselect.tags}` | Select (multiple) | Çoklu seçim | json (array) | - |
+| `{checkbox.agree}` | Checkbox | Tekli checkbox | boolean | - |
+| `{checkboxlist.options}` | CheckboxList | Checkbox listesi | json (array) | - |
+| `{radio.gender}` | Radio | Radio button | string/int | - |
+| `{toggle.active}` | Toggle | Açık/Kapalı | boolean | - |
+| `{togglebuttons.size}` | ToggleButtons | Buton grubu | string | - |
+| `{color.theme}` | ColorPicker | Renk seçici | string (hex) | 7 char |
+| `{tags.keywords}` | TagsInput | Etiket girişi | json (array) | - |
+| `{keyvalue.meta}` | KeyValue | Anahtar-değer | json (object) | - |
+| `{slider.volume}` | Slider | Kaydırıcı | integer | - |
+| `{hidden.calc}` | Hidden | Gizli alan | mixed | - |
+
+#### 2.2 Örnek Template (Comprehensive)
+```html
+
+
+
+
+```
+
+**Bu template şu form alanlarını oluşturur:**
+- Logo (FileUpload - image)
+- Company Name (TextInput)
+- Tagline (Textarea)
+- Menu HTML (RichEditor)
+- Contact Email (TextInput - email)
+- Phone (TextInput - tel)
+- Facebook Link (TextInput - url)
+- Twitter Link (TextInput - url)
+- LinkedIn Link (TextInput - url)
+- Show Search (Toggle)
+- Show Cart (Toggle)
+- Header BG (ColorPicker)
+- Padding (TextInput - number)
+- Opacity (Slider)
+
+### 3. Pages Modülü Entegrasyonu
+
+#### 3.1 Pages Tablo Güncellemesi
+```php
+// Migration: add_template_fields_to_pages_table
+Schema::table('pages', function (Blueprint $table) {
+ $table->foreignId('header_template_id')->nullable()->constrained('header_templates')->nullOnDelete();
+ $table->json('header_data')->nullable(); // Header placeholder değerleri
+
+ $table->foreignId('footer_template_id')->nullable()->constrained('footer_templates')->nullOnDelete();
+ $table->json('footer_data')->nullable(); // Footer placeholder değerleri
+
+ // sections_data -> Repeater ile sections (eski page_sections yerine)
+ // Format: [
+ // {
+ // 'section_template_id': 1,
+ // 'section_data': { 'text.title': 'Başlık', 'image.banner': 'path/to/image.jpg' }
+ // },
+ // ...
+ // ]
+ $table->json('sections_data')->nullable();
+});
+```
+
+**Veri Tipi Notları:**
+- `json` column tipi: MySQL 5.7.8+ ve PostgreSQL 9.4+ destekli
+- JSON alanlar Laravel tarafından otomatik encode/decode edilir
+- `longtext` alternatifi: Tüm veritabanlarıyla uyumlu (Laravel JSON cast ile)
+- Her iki durumda da Laravel'in `$casts = ['header_data' => 'array']` özelliği kullanılır
+
+#### 3.2 Page Model İlişkileri
+```php
+class Page extends Model
+{
+ public function headerTemplate()
+ {
+ return $this->belongsTo(HeaderTemplate::class);
+ }
+
+ public function footerTemplate()
+ {
+ return $this->belongsTo(FooterTemplate::class);
+ }
+
+ // sections_data JSON içinde section_template_id'ler var
+ public function getSectionsAttribute()
+ {
+ $sectionsData = $this->sections_data ?? [];
+ return collect($sectionsData)->map(function ($section) {
+ return [
+ 'template' => SectionTemplate::find($section['section_template_id']),
+ 'data' => $section['section_data'] ?? [],
+ ];
+ });
+ }
+}
+```
+
+### 4. Filament Form Yapısı
+
+#### 4.1 Page Form (PageForm.php)
+```php
+use Filament\Schemas\Components\Tabs;
+use Filament\Schemas\Components\Tabs\Tab;
+use Filament\Forms\Components\Select;
+use Filament\Forms\Components\Repeater;
+
+Tabs::make('PageTabs')
+ ->tabs([
+ Tab::make('Genel Bilgiler')
+ ->schema([
+ TextInput::make('title')->required(),
+ TextInput::make('slug')->required(),
+ // ... diğer genel alanlar
+ ]),
+
+ Tab::make('Header Template')
+ ->schema([
+ Select::make('header_template_id')
+ ->label('Header Template')
+ ->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
+ ->live()
+ ->afterStateUpdated(fn ($state, Set $set) => $set('header_data', [])),
+
+ // Dinamik header alanları
+ Group::make()
+ ->schema(fn (Get $get) => self::generateDynamicFields(
+ HeaderTemplate::find($get('header_template_id')),
+ 'header_data'
+ ))
+ ->visible(fn (Get $get) => filled($get('header_template_id'))),
+ ]),
+
+ Tab::make('Sections')
+ ->schema([
+ Repeater::make('sections_data')
+ ->schema([
+ Select::make('section_template_id')
+ ->label('Section Template')
+ ->options(SectionTemplate::where('is_active', true)->pluck('title', 'id'))
+ ->live()
+ ->required(),
+
+ // Dinamik section alanları
+ Group::make()
+ ->schema(fn (Get $get, $record) => self::generateDynamicFields(
+ SectionTemplate::find($get('section_template_id')),
+ 'section_data'
+ ))
+ ->visible(fn (Get $get) => filled($get('section_template_id'))),
+ ])
+ ->itemLabel(fn (array $state): ?string =>
+ SectionTemplate::find($state['section_template_id'] ?? null)?->title ?? 'Section'
+ )
+ ->collapsible()
+ ->reorderable()
+ ->addActionLabel(__('pages.add_section')),
+ ]),
+
+ Tab::make('Footer Template')
+ ->schema([
+ Select::make('footer_template_id')
+ ->label('Footer Template')
+ ->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
+ ->live()
+ ->afterStateUpdated(fn ($state, Set $set) => $set('footer_data', [])),
+
+ // Dinamik footer alanları
+ Group::make()
+ ->schema(fn (Get $get) => self::generateDynamicFields(
+ FooterTemplate::find($get('footer_template_id')),
+ 'footer_data'
+ ))
+ ->visible(fn (Get $get) => filled($get('footer_template_id'))),
+ ]),
+ ]);
+```
+
+#### 4.2 Dinamik Form Generator
+```php
+use Filament\Forms\Components\{
+ TextInput, Textarea, Select, Checkbox, CheckboxList,
+ Radio, Toggle, ToggleButtons, DateTimePicker, DatePicker,
+ TimePicker, FileUpload, RichEditor, MarkdownEditor,
+ ColorPicker, TagsInput, KeyValue, CodeEditor, Hidden, Slider
+};
+
+protected static function generateDynamicFields($template, string $dataKey): array
+{
+ if (!$template) return [];
+
+ $placeholders = self::parsePlaceholders($template->html_content);
+ $fields = [];
+
+ foreach ($placeholders as $placeholder) {
+ [$type, $name] = explode('.', $placeholder);
+
+ $label = str($name)->title()->replace('_', ' ')->toString();
+
+ $field = match($type) {
+ // Text Input Variants
+ 'text' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->maxLength(255),
+
+ 'email' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->email()
+ ->maxLength(255),
+
+ 'url' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->url()
+ ->maxLength(500),
+
+ 'tel' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->tel()
+ ->maxLength(20),
+
+ 'number' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->numeric(),
+
+ 'password' => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->password()
+ ->dehydrated(fn ($state) => filled($state))
+ ->maxLength(255),
+
+ // Text Area & Editors
+ 'textarea' => Textarea::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->rows(4)
+ ->maxLength(5000),
+
+ 'richtext' => RichEditor::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->toolbarButtons([
+ 'bold', 'italic', 'underline', 'link',
+ 'bulletList', 'orderedList', 'h2', 'h3',
+ ])
+ ->maxLength(50000),
+
+ 'markdown' => MarkdownEditor::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->toolbarButtons([
+ 'bold', 'italic', 'strike', 'link',
+ 'heading', 'bulletList', 'orderedList', 'codeBlock',
+ ])
+ ->maxLength(50000),
+
+ 'code' => CodeEditor::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ //->lineNumbers()
+ ->maxLength(50000),
+
+ // Date & Time
+ 'date' => DatePicker::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->native(false),
+
+ 'datetime' => DateTimePicker::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->native(false)
+ ->seconds(false),
+
+ 'time' => TimePicker::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->native(false)
+ ->seconds(false),
+
+ // File Uploads
+ 'image' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->image()
+ ->imageEditor()
+ ->imageEditorAspectRatios([
+ null,
+ '16:9',
+ '4:3',
+ '1:1',
+ ])
+ ->directory('templates/images')
+ ->maxSize(5120),
+
+ 'images' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->image()
+ ->multiple()
+ ->imageEditor()
+ ->directory('templates/images')
+ ->maxSize(5120)
+ ->maxFiles(10),
+
+ 'file' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->directory('templates/files')
+ ->maxSize(10240),
+
+ 'files' => FileUpload::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->multiple()
+ ->directory('templates/files')
+ ->maxSize(10240)
+ ->maxFiles(10),
+
+ // Selection & Options
+ 'select' => Select::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->searchable(),
+
+ 'multiselect' => Select::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->multiple()
+ ->options(self::getSelectOptions($name))
+ ->searchable(),
+
+ 'checkbox' => Checkbox::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->inline(false),
+
+ 'checkboxlist' => CheckboxList::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->columns(2),
+
+ 'radio' => Radio::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->inline()
+ ->inlineLabel(false),
+
+ 'toggle' => Toggle::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->inline(false),
+
+ 'togglebuttons' => ToggleButtons::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->options(self::getSelectOptions($name))
+ ->inline()
+ ->grouped(),
+
+ // Color & Visual
+ 'color' => ColorPicker::make("{$dataKey}.{$placeholder}")
+ ->label($label),
+
+ // Structured Data
+ 'tags' => TagsInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->separator(','),
+
+ 'keyvalue' => KeyValue::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->keyLabel('Key')
+ ->valueLabel('Value')
+ ->reorderable(),
+
+ // Advanced
+ 'slider' => Slider::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->minValue(0)
+ ->maxValue(100)
+ ->step(1),
+
+ 'hidden' => Hidden::make("{$dataKey}.{$placeholder}"),
+
+ // Default
+ default => TextInput::make("{$dataKey}.{$placeholder}")
+ ->label($label)
+ ->maxLength(255)
+ ->helperText("Unknown type: {$type}"),
+ };
+
+ $fields[] = $field;
+ }
+
+ return $fields;
+}
+
+/**
+ * Select, MultiSelect, CheckboxList, Radio, ToggleButtons için
+ * dinamik options yükler. Gerçek uygulamada config veya database'den
+ * okunabilir.
+ */
+protected static function getSelectOptions(string $fieldName): array
+{
+ // Örnek: config/template-options.php dosyasından okuyabilirsiniz
+ return config("template-options.{$fieldName}", [
+ 'option_1' => 'Option 1',
+ 'option_2' => 'Option 2',
+ 'option_3' => 'Option 3',
+ ]);
+}
+
+/**
+ * Template HTML içeriğindeki placeholder'ları parse eder
+ * Desteklenen format: {type.field_name}
+ */
+protected static function parsePlaceholders(string $html): array
+{
+ // Regex: {word.word_with_underscores}
+ preg_match_all('/\{([a-z]+\.[a-z_]+)\}/i', $html, $matches);
+ return array_unique($matches[1] ?? []);
+}
+```
+
+### 5. Frontend Rendering
+
+#### 5.1 Blade Component (PageRenderer)
+```php
+// app/View/Components/PageRenderer.php
+class PageRenderer extends Component
+{
+ public function __construct(public Page $page) {}
+
+ public function render()
+ {
+ return view('components.page-renderer');
+ }
+
+ public function renderHeader()
+ {
+ if (!$this->page->headerTemplate) return '';
+
+ return $this->replaceePlaceholders(
+ $this->page->headerTemplate->html_content,
+ $this->page->header_data ?? []
+ );
+ }
+
+ public function renderSections()
+ {
+ return collect($this->page->sections)->map(function ($section) {
+ return $this->replacePlaceholders(
+ $section['template']->html_content,
+ $section['data']
+ );
+ })->implode('');
+ }
+
+ public function renderFooter()
+ {
+ if (!$this->page->footerTemplate) return '';
+
+ return $this->replacePlaceholders(
+ $this->page->footerTemplate->html_content,
+ $this->page->footer_data ?? []
+ );
+ }
+
+ protected function replacePlaceholders(string $html, array $data): string
+ {
+ foreach ($data as $placeholder => $value) {
+ // {text.name} → value
+ $html = str_replace("{{$placeholder}}", $value, $html);
+ }
+ return $html;
+ }
+}
+```
+
+#### 5.2 Blade View
+```blade
+
+
+ {!! $renderHeader() !!}
+
+
+ {!! $renderSections() !!}
+
+
+ {!! $renderFooter() !!}
+
+```
+
+### 6. Navigasyon Yapısı
+
+```php
+// Filament Panel Provider
+->navigationGroups([
+ 'Template' => [
+ 'icon' => 'heroicon-o-document-duplicate',
+ 'order' => 3,
+ ],
+])
+
+// Her Resource'da
+public static function getNavigationGroup(): string
+{
+ return 'Template';
+}
+```
+
+## Dosya Yapısı
+
+```
+app/
+├── Models/
+│ ├── HeaderTemplate.php
+│ ├── SectionTemplate.php
+│ └── FooterTemplate.php
+├── Filament/Admin/Resources/
+│ ├── HeaderTemplates/
+│ │ ├── HeaderTemplateResource.php
+│ │ ├── Pages/
+│ │ ├── Schemas/
+│ │ └── Tables/
+│ ├── SectionTemplates/
+│ │ ├── SectionTemplateResource.php
+│ │ ├── Pages/
+│ │ ├── Schemas/
+│ │ └── Tables/
+│ └── FooterTemplates/
+│ ├── FooterTemplateResource.php
+│ ├── Pages/
+│ ├── Schemas/
+│ └── Tables/
+├── View/Components/
+│ └── PageRenderer.php
+└── Services/
+ └── TemplateService.php (helper methods)
+
+database/migrations/
+├── create_header_templates_table.php
+├── create_section_templates_table.php
+├── create_footer_templates_table.php
+└── add_template_fields_to_pages_table.php
+
+lang/
+├── tr/
+│ ├── header-templates.php
+│ ├── section-templates.php
+│ └── footer-templates.php
+└── en/
+ ├── header-templates.php
+ ├── section-templates.php
+ └── footer-templates.php
+
+resources/views/
+└── components/
+ └── page-renderer.blade.php
+```
+
+## Örnek Kullanım Senaryosu
+
+### 1. Template Tanımlama
+**Header Template:**
+```html
+
+```
+
+### 2. Page'de Template Seçimi
+- Header Template: "Ana Header" seçildi
+- Dinamik alanlar açıldı:
+ - Logo (FileUpload)
+ - Site Name (TextInput)
+ - Menu HTML (RichEditor)
+ - Contact Email (TextInput)
+- Values dolduruldu ve kaydedildi
+
+### 3. Frontend'de Görüntüleme
+```blade
+
+```
+
+Output:
+```html
+
+```
+
+## Avantajlar
+
+1. ✅ **Tamamen Dinamik**: Kod değişikliği olmadan yeni template'ler eklenebilir
+2. ✅ **Yeniden Kullanılabilir**: Aynı template birden fazla sayfada kullanılabilir
+3. ✅ **Kolay Yönetim**: Admin panel üzerinden HTML düzenlenebilir
+4. ✅ **Tip Güvenli**: Placeholder'lar form tipini belirler
+5. ✅ **Ölçeklenebilir**: Yeni form tipleri kolayca eklenebilir
+6. ✅ **SEO Dostu**: Her sayfa kendi içeriğine sahip
+7. ✅ **Performanslı**: Sadece seçili template'ler yüklenir
+8. ✅ **30+ Form Tipi**: Filament 4.x'in tüm form component'leri destekleniyor
+9. ✅ **Zengin İçerik**: HTML, Markdown, Code editor desteği
+10. ✅ **Büyük Veri**: longtext ile 4GB kapasiteli template'ler
+
+## Veri Tipi Özeti
+
+### HTML Content (Template'ler için)
+```sql
+html_content LONGTEXT
+-- Kapasite: 4,294,967,295 karakter (4GB)
+-- Kullanım: Template HTML + placeholder'lar
+-- Avantaj: En karmaşık template'ler için bile yeterli
+```
+
+### JSON Data (Page verisi için)
+```sql
+header_data JSON
+footer_data JSON
+sections_data JSON
+-- MySQL 5.7.8+ ve PostgreSQL 9.4+ native JSON desteği
+-- Alternatif: LONGTEXT + Laravel JSON cast
+-- Laravel otomatik encode/decode yapar
+```
+
+### Model Cast Kullanımı
+```php
+protected $casts = [
+ 'header_data' => 'array',
+ 'footer_data' => 'array',
+ 'sections_data' => 'array',
+];
+```
+
+## Git Branch
+
+```bash
+feature/dynamic-template-system
+```
+
+## Menü Template Sistemi
+
+### Kurulum
+
+Menü template sistemi global bir ayardır. Admin panelden oluşturduğunuz menü template'ini Settings'e manuel olarak eklemeniz gerekir:
+
+#### 1. Admin Panelde Menü Template Oluştur
+```
+Admin Panel → Menü Şablonları → Yeni Menü Şablonu
+```
+
+**Örnek Menü Template:**
+```html
+
+```
+
+#### 2. Settings'e Menü Template ID Ekle
+Admin panelden yeni bir Setting oluşturun:
+- **Key:** `menu_template_id`
+- **Type:** `integer`
+- **Value:** Menü template ID'niz (örn: 1, 2, 3)
+- **Group:** `general`
+
+#### 3. Kullanım
+
+Menü otomatik olarak render edilir. Header template'inde `{menu}` placeholder'ı kullanabilirsiniz:
+
+**Header Template Örneği:**
+```html
+
+```
+
+**Çıktı:**
+```html
+
+```
+
+### Menü Rendering
+
+Menü otomatik olarak Pages tablosundan alınır:
+- Sadece `status = 'published'` olanlar
+- Sadece `show_in_menu = true` olanlar
+- Hierarchical yapı korunur (parent_id ile)
+- `sort_order` ile sıralanır
+
+### Cache
+
+Menü öğeleri 1 saat süreyle cache'lenir. Cache'i temizlemek için:
+
+```php
+use App\Services\MenuService;
+
+MenuService::clearCache();
+```
+
+## İmplement Adımları
+
+1. ✅ Migrations oluştur (4 adet)
+2. ✅ Model'ler oluştur (4 adet)
+3. ✅ Filament Resources oluştur (4 adet)
+4. ✅ TemplateService helper oluştur
+5. ✅ PageForm'u güncelle (template tabs ekle)
+6. ✅ PageRenderer component oluştur
+7. ✅ Lang dosyaları oluştur (8 adet)
+8. ✅ Frontend route ve controller güncelle
+9. ✅ Menü Service eklendi
+10. ✅ Test
+
+---
+
+**Not:** Bu sistem mevcut `page_sections` sisteminin yerini alacak ve çok daha güçlü bir alternatif sunacaktır.
+
diff --git a/docs/SECTION_BUILDER.md b/docs/SECTION_BUILDER.md
new file mode 100644
index 0000000..60bbc76
--- /dev/null
+++ b/docs/SECTION_BUILDER.md
@@ -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 Web" │
+├─────────────────────────────────────────┤
+│ 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 Web',
+ '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')
+
+
+ @elseif($section['type'] === 'features')
+
+
+ @elseif($section['type'] === 'stats')
+
+
+ @endif
+@endforeach
+```
+
+### Component'te Data Kullanımı
+
+```blade
+{{-- resources/views/components/blocks/hero.blade.php --}}
+
+@props(['data'])
+
+
+
+ @if(isset($data['badge']))
+
{{ $data['badge'] }}
+ @endif
+
+ @if(isset($data['title']))
+
{!! $data['title'] !!}
+ @endif
+
+ @if(isset($data['subtitle']))
+
{{ $data['subtitle'] }}
+ @endif
+
+ @if(isset($data['button_text']) && isset($data['button_url']))
+
+ {{ $data['button_text'] }}
+
+ @endif
+
+
+```
+
+## 🎨 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
+
+
+ @includeIf("components.blocks.{$section['type']}", compact('data'))
+
+@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.
+
diff --git a/docs/SEEDER_USAGE.md b/docs/SEEDER_USAGE.md
new file mode 100644
index 0000000..5030bd2
--- /dev/null
+++ b/docs/SEEDER_USAGE.md
@@ -0,0 +1,255 @@
+# Veritabanı Seeder Kullanım Kılavuzu
+
+## Genel Bakış
+
+Sistemde 2 ana seeder bulunmaktadır:
+1. **PageSeeder** - Örnek sayfalar (home, about, services, contact, pricing)
+2. **SettingSeeder** - Site ayarları ve yapılandırmaları
+
+## Seeder'ları Çalıştırma
+
+### Tüm Seeder'ları Çalıştırma
+
+```bash
+php artisan db:seed
+```
+
+### Sadece Page Seeder'ı Çalıştırma
+
+```bash
+php artisan db:seed --class=PageSeeder
+```
+
+### Sadece Setting Seeder'ı Çalıştırma
+
+```bash
+php artisan db:seed --class=SettingSeeder
+```
+
+### Fresh Migration + Seed (Dikkat: Tüm veriler silinir!)
+
+```bash
+php artisan migrate:fresh --seed
+```
+
+---
+
+## PageSeeder İçeriği
+
+### Oluşturulan Sayfalar
+
+1. **Anasayfa** (`slug: home`)
+ - `is_homepage: true`
+ - `template: home`
+ - Bloklar: Hero, Features, Stats, CTA
+ - Durum: published
+
+2. **Hakkımızda** (`slug: about`)
+ - `template: generic`
+ - Bloklar: Hero, Features
+ - Durum: published
+
+3. **Hizmetlerimiz** (`slug: services`)
+ - `template: generic`
+ - Bloklar: Hero, Features (6 hizmet)
+ - Durum: published
+
+4. **İletişim** (`slug: contact`)
+ - `template: generic`
+ - Bloklar: Hero, Contact Form
+ - Durum: published
+
+5. **Fiyatlandırma** (`slug: pricing`)
+ - `template: generic`
+ - Bloklar: Hero, Pricing (3 paket)
+ - Durum: published
+ - `show_in_menu: false` (menüde görünmez)
+
+### Blok Yapısı Örneği
+
+```json
+{
+ "sections": [
+ {
+ "type": "hero",
+ "data": {
+ "badge": "YENİ PLATFORM",
+ "title": "Modern ve Çok Amaçlı Web Çözümleri",
+ "subtitle": "...",
+ "primary_button_text": "Hemen Başla",
+ "primary_button_url": "#features"
+ }
+ },
+ {
+ "type": "features",
+ "data": {
+ "section_title": "Neden Truncgil Citrus?",
+ "features": [...]
+ }
+ }
+ ]
+}
+```
+
+---
+
+## SettingSeeder İçeriği
+
+### Gruplar ve Ayarlar
+
+| Grup | Açıklama | Örnek Ayarlar |
+|------|----------|---------------|
+| `general` | Genel site ayarları | site_name, site_description, logo, favicon |
+| `theme` | Tema ve renk ayarları | primary_color, secondary_color, custom_css |
+| `localization` | Dil ve bölge ayarları | timezone, currency, date_format |
+| `email` | E-posta sunucu ayarları | mail_host, mail_port, mail_from |
+| `social` | Sosyal medya linkleri | facebook, instagram, linkedin, twitter |
+| `layout` | Frontend layout ayarları | default_meta_title, logo_path, favicon_path |
+| `seo` | SEO ve analitik ayarları | google_analytics, meta_keywords |
+| `security` | Güvenlik ayarları | max_login_attempts, password_rules |
+| `upload` | Dosya yükleme ayarları | max_size, allowed_types, image_quality |
+| `notification` | Bildirim ayarları | email_enabled, sms_enabled |
+| `cache` | Cache ayarları | cache_driver, cache_duration |
+| `payment` | Ödeme ayarları | default_gateway, test_mode |
+| `api` | API ayarları | rate_limit, auth_method |
+| `logging` | Log ayarları | log_level, retention_days |
+| `performance` | Performans ayarları | query_cache, cdn_enabled |
+| `integration` | 3. parti entegrasyonlar | recaptcha, stripe, paypal |
+
+### Frontend için Kritik Ayarlar
+
+```php
+// Layout için
+'default_meta_title' => 'Truncgil Citrus - Modern Web Çözümleri'
+'default_meta_description' => 'Yenilikçi teknoloji çözümleri...'
+'default_meta_image' => 'assets/img/demos/f1.png'
+'logo_path' => 'assets/img/truncgil-yatay.svg'
+'favicon_path' => 'assets/img/favicon.ico'
+
+// Sosyal medya
+'social_links' => {
+ 'facebook': 'https://facebook.com/',
+ 'instagram': 'https://instagram.com/',
+ 'linkedin': 'https://linkedin.com/',
+ 'twitter': 'https://twitter.com/'
+}
+```
+
+---
+
+## Seeder Sonrası Kontrol
+
+### 1. Veritabanı Kontrolü
+
+```bash
+# Pages tablosunu kontrol et
+php artisan tinker
+>>> App\Models\Page::count()
+=> 5
+
+>>> App\Models\Page::where('status', 'published')->count()
+=> 5
+
+>>> App\Models\Page::where('is_homepage', true)->first()->slug
+=> "home"
+```
+
+### 2. Setting Kontrolü
+
+```bash
+php artisan tinker
+>>> App\Models\Setting::where('key', 'default_meta_title')->first()->value
+=> "Truncgil Citrus - Modern Web Çözümleri"
+
+>>> App\Models\Setting::where('key', 'social_links')->first()->value
+=> "{\"facebook\":\"https://facebook.com/\",...}"
+```
+
+### 3. Frontend Kontrolü
+
+- `https://citrus.truncgil.com/` → Anasayfa (home template + bloklar)
+- `https://citrus.truncgil.com/about` → Hakkımızda
+- `https://citrus.truncgil.com/services` → Hizmetler
+- `https://citrus.truncgil.com/contact` → İletişim
+- `https://citrus.truncgil.com/pricing` → Fiyatlandırma
+
+---
+
+## Özelleştirme
+
+### Yeni Sayfa Ekleme
+
+`database/seeders/PageSeeder.php` içinde `$pages` dizisine ekleyin:
+
+```php
+[
+ 'title' => 'Portfolyo',
+ 'slug' => 'portfolio',
+ 'template' => 'generic',
+ 'status' => 'published',
+ 'show_in_menu' => true,
+ 'sort_order' => 6,
+ 'published_at' => now(),
+ 'sections' => [
+ // Bloklar...
+ ],
+]
+```
+
+### Var Olan Sayfayı Güncelleme
+
+Seeder updateOrCreate kullanıyor, slug'a göre günceller:
+
+```bash
+php artisan db:seed --class=PageSeeder
+```
+
+Aynı slug varsa güncellenir, yoksa yeni kayıt oluşturulur.
+
+### Setting Değerlerini Değiştirme
+
+`database/seeders/SettingSeeder.php` içinde ilgili `value` alanını değiştirin ve çalıştırın:
+
+```bash
+php artisan db:seed --class=SettingSeeder
+```
+
+---
+
+## Sorun Giderme
+
+### "Class not found" Hatası
+
+```bash
+composer dump-autoload
+php artisan clear-compiled
+php artisan config:clear
+php artisan db:seed
+```
+
+### "Table doesn't exist" Hatası
+
+```bash
+php artisan migrate
+php artisan db:seed
+```
+
+### Seeder Çalışmıyor
+
+```bash
+# Verbose mode ile hata detaylarını görün
+php artisan db:seed --class=PageSeeder -vvv
+```
+
+### Fresh Start (Tüm veriyi sıfırlama)
+
+```bash
+php artisan migrate:fresh --seed
+```
+
+⚠️ **Uyarı:** Bu komut tüm tabloları siler ve yeniden oluşturur!
+
+---
+
+**Son Güncelleme:** {{ now()->format('d.m.Y H:i') }}
+
diff --git a/lang/en/blog.php b/lang/en/blog.php
index 8d98641..0f06497 100644
--- a/lang/en/blog.php
+++ b/lang/en/blog.php
@@ -1,6 +1,13 @@
'Blog',
+ 'meta-index-title' => 'Blog',
+ 'meta-index-description' => 'Latest posts',
+ 'empty' => 'No posts yet.',
+
+ // Module labels
'title' => 'Blog Posts',
'navigation_label' => 'Blog',
'model_label' => 'Blog Post',
@@ -9,6 +16,8 @@ return [
// Actions
'create' => 'Create New Blog Post',
'edit' => 'Edit Blog Post',
+ 'save' => 'Save',
+ 'cancel' => 'Cancel',
'delete' => 'Delete Blog Post',
'restore' => 'Restore Blog Post',
'force_delete' => 'Force Delete',
diff --git a/lang/en/footer-templates.php b/lang/en/footer-templates.php
new file mode 100644
index 0000000..1ef0b78
--- /dev/null
+++ b/lang/en/footer-templates.php
@@ -0,0 +1,43 @@
+ 'Templates',
+ 'navigation_label' => 'Footer',
+ 'model_label' => 'Footer Template',
+ 'plural_model_label' => 'Footer Templates',
+
+ // Sections
+ 'section_general' => 'General Information',
+ 'section_default_data' => 'Default Values',
+ 'section_default_data_desc' => 'You can enter default values for placeholders when creating templates. These values will be automatically loaded when the template is selected on pages.',
+
+ // Form Fields
+ 'field_title' => 'Title',
+ 'field_html_content' => 'HTML Content',
+ 'field_html_content_help' => 'Placeholder format: {type.field_name} e.g: {text.copyright}, {richtext.links}, {email.contact}',
+ 'field_is_active' => 'Active',
+
+ // Table Columns
+ 'column_title' => 'Title',
+ 'column_is_active' => 'Active',
+ 'column_pages_count' => 'Pages Count',
+ 'column_created_at' => 'Created At',
+ 'column_updated_at' => 'Updated At',
+ 'column_deleted_at' => 'Deleted At',
+
+ // Actions
+ 'create' => 'New Footer Template',
+ 'edit' => 'Edit Footer Template',
+ 'view' => 'View Footer Template',
+ 'delete' => 'Delete',
+ 'restore' => 'Restore',
+ 'force_delete' => 'Force Delete',
+
+ // Messages
+ 'created_successfully' => 'Footer template created successfully.',
+ 'updated_successfully' => 'Footer template updated successfully.',
+ 'deleted_successfully' => 'Footer template deleted successfully.',
+ 'restored_successfully' => 'Footer template restored successfully.',
+];
+
diff --git a/lang/en/header-templates.php b/lang/en/header-templates.php
new file mode 100644
index 0000000..451ffa1
--- /dev/null
+++ b/lang/en/header-templates.php
@@ -0,0 +1,43 @@
+ 'Templates',
+ 'navigation_label' => 'Header',
+ 'model_label' => 'Header Template',
+ 'plural_model_label' => 'Header Templates',
+
+ // Sections
+ 'section_general' => 'General Information',
+ 'section_default_data' => 'Default Values',
+ 'section_default_data_desc' => 'You can enter default values for placeholders when creating templates. These values will be automatically loaded when the template is selected on pages.',
+
+ // Form Fields
+ 'field_title' => 'Title',
+ 'field_html_content' => 'HTML Content',
+ 'field_html_content_help' => 'Placeholder format: {type.field_name} e.g: {text.company_name}, {image.logo}, {email.contact}',
+ 'field_is_active' => 'Active',
+
+ // Table Columns
+ 'column_title' => 'Title',
+ 'column_is_active' => 'Active',
+ 'column_pages_count' => 'Pages Count',
+ 'column_created_at' => 'Created At',
+ 'column_updated_at' => 'Updated At',
+ 'column_deleted_at' => 'Deleted At',
+
+ // Actions
+ 'create' => 'New Header Template',
+ 'edit' => 'Edit Header Template',
+ 'view' => 'View Header Template',
+ 'delete' => 'Delete',
+ 'restore' => 'Restore',
+ 'force_delete' => 'Force Delete',
+
+ // Messages
+ 'created_successfully' => 'Header template created successfully.',
+ 'updated_successfully' => 'Header template updated successfully.',
+ 'deleted_successfully' => 'Header template deleted successfully.',
+ 'restored_successfully' => 'Header template restored successfully.',
+];
+
diff --git a/lang/en/language.php b/lang/en/language.php
index d152b74..e3588c6 100644
--- a/lang/en/language.php
+++ b/lang/en/language.php
@@ -10,6 +10,8 @@ return [
// Actions
'create' => 'Create Language',
'edit' => 'Edit Language',
+ 'save' => 'Save',
+ 'cancel' => 'Cancel',
'delete' => 'Delete Language',
'restore' => 'Restore',
'force_delete' => 'Force Delete',
diff --git a/lang/en/menu-templates.php b/lang/en/menu-templates.php
new file mode 100644
index 0000000..1726c06
--- /dev/null
+++ b/lang/en/menu-templates.php
@@ -0,0 +1,42 @@
+ 'Templates',
+ 'navigation_label' => 'Menu Templates',
+ 'model_label' => 'Menu Template',
+ 'plural_model_label' => 'Menu Templates',
+
+ // Sections
+ 'section_general' => 'General Information',
+ 'section_structure' => 'Menu Structure',
+ 'section_structure_desc' => 'You can edit the menu structure using drag-and-drop or write code in JSON format.',
+
+ // Form Fields
+ 'field_title' => 'Title',
+ 'field_html_content' => 'Menu HTML Code',
+ 'field_html_content_help' => 'You can add the menu structure to the HTML using the {menu} placeholder. The menu will be automatically generated with page-based rendering.',
+ 'field_is_active' => 'Active',
+
+ // Table Columns
+ 'column_title' => 'Title',
+ 'column_is_active' => 'Active',
+ 'column_created_at' => 'Created At',
+ 'column_updated_at' => 'Updated At',
+ 'column_deleted_at' => 'Deleted At',
+
+ // Actions
+ 'create' => 'New Menu Template',
+ 'edit' => 'Edit Menu Template',
+ 'view' => 'View Menu Template',
+ 'delete' => 'Delete',
+ 'restore' => 'Restore',
+ 'force_delete' => 'Force Delete',
+
+ // Messages
+ 'created_successfully' => 'Menu template created successfully.',
+ 'updated_successfully' => 'Menu template updated successfully.',
+ 'deleted_successfully' => 'Menu template deleted successfully.',
+ 'restored_successfully' => 'Menu template restored successfully.',
+];
+
diff --git a/lang/en/pages.php b/lang/en/pages.php
index d70fc7a..960b536 100644
--- a/lang/en/pages.php
+++ b/lang/en/pages.php
@@ -1,6 +1,13 @@
'Home',
+ 'nav-about' => 'About',
+ 'nav-services' => 'Services',
+ 'nav-contact' => 'Contact',
+
+ // Module labels
'title' => 'Pages',
'navigation_label' => 'Pages',
'model_label' => 'Page',
@@ -9,23 +16,24 @@ return [
// Actions
'create' => 'Create New Page',
'edit' => 'Edit Page',
+ 'view' => 'View',
+ 'save' => 'Save',
+ 'cancel' => 'Cancel',
'delete' => 'Delete Page',
'restore' => 'Restore Page',
'force_delete' => 'Force Delete',
- // Form fields
- 'title_field' => 'Title',
- 'slug_field' => 'URL Slug',
- 'content_field' => 'Content',
- 'meta_title_field' => 'Meta Title',
- 'meta_description_field' => 'Meta Description',
- 'status_field' => 'Status',
- 'published_at_field' => 'Published At',
+ // Menu Tree
+ 'menu_tree_title' => 'Menu Structure',
+ 'menu_refresh' => 'Refresh',
+ 'menu_back_to_pages' => 'Back to Pages',
+ 'menu_tree_saved' => 'Menu structure saved successfully.',
// Form sections
'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
@@ -97,4 +105,69 @@ 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',
+
+ // Dynamic Template System
+ 'form_section_header_template' => 'Header Template',
+ 'form_section_header_template_desc' => 'Select a dynamic header template for the top of the page',
+ 'header_template_field' => 'Header Template',
+
+ 'form_section_template_sections' => 'Template Sections',
+ 'form_section_template_sections_desc' => 'Add dynamic template sections for the page',
+ 'template_sections_field' => 'Template Sections',
+ 'section_template_field' => 'Section Template',
+ 'add_template_section' => 'Add Section',
+ 'template_section' => 'Template Section',
+
+ 'form_section_footer_template' => 'Footer Template',
+ 'form_section_footer_template_desc' => 'Select a dynamic footer template for the bottom of the page',
+ 'footer_template_field' => 'Footer Template',
];
diff --git a/lang/en/placeholder-picker.php b/lang/en/placeholder-picker.php
new file mode 100644
index 0000000..48b4085
--- /dev/null
+++ b/lang/en/placeholder-picker.php
@@ -0,0 +1,37 @@
+ 'Placeholder Variables',
+ 'click_to_insert' => 'Click to insert into editor',
+ 'special_placeholders' => 'Special Placeholders',
+ 'custom_placeholders' => 'Custom Placeholders',
+ 'search_placeholder' => 'Search placeholders...',
+ 'no_results' => 'No results found',
+
+ // Placeholder Types
+ 'type_text' => 'Text',
+ 'type_email' => 'Email',
+ 'type_url' => 'URL',
+ 'type_tel' => 'Telephone',
+ 'type_number' => 'Number',
+ 'type_textarea' => 'Textarea',
+ 'type_richtext' => 'Rich Text',
+ 'type_markdown' => 'Markdown',
+ 'type_code' => 'Code',
+ 'type_date' => 'Date',
+ 'type_datetime' => 'Date & Time',
+ 'type_time' => 'Time',
+ 'type_image' => 'Image',
+ 'type_images' => 'Images (Multiple)',
+ 'type_file' => 'File',
+ 'type_files' => 'Files (Multiple)',
+ 'type_select' => 'Select',
+ 'type_multiselect' => 'Multi Select',
+ 'type_checkbox' => 'Checkbox',
+ 'type_radio' => 'Radio',
+ 'type_toggle' => 'Toggle',
+ 'type_color' => 'Color',
+ 'type_tags' => 'Tags',
+ 'type_custom' => 'Custom',
+];
+
diff --git a/lang/en/section-templates.php b/lang/en/section-templates.php
new file mode 100644
index 0000000..58b847d
--- /dev/null
+++ b/lang/en/section-templates.php
@@ -0,0 +1,42 @@
+ 'Templates',
+ 'navigation_label' => 'Section',
+ 'model_label' => 'Section Template',
+ 'plural_model_label' => 'Section Templates',
+
+ // Sections
+ 'section_general' => 'General Information',
+ 'section_default_data' => 'Default Values',
+ 'section_default_data_desc' => 'You can enter default values for placeholders when creating templates. These values will be automatically loaded when the template is selected on pages.',
+
+ // Form Fields
+ 'field_title' => 'Title',
+ 'field_html_content' => 'HTML Content',
+ 'field_html_content_help' => 'Placeholder format: {type.field_name} e.g: {text.title}, {richtext.content}, {image.banner}',
+ 'field_is_active' => 'Active',
+
+ // Table Columns
+ 'column_title' => 'Title',
+ 'column_is_active' => 'Active',
+ 'column_created_at' => 'Created At',
+ 'column_updated_at' => 'Updated At',
+ 'column_deleted_at' => 'Deleted At',
+
+ // Actions
+ 'create' => 'New Section Template',
+ 'edit' => 'Edit Section Template',
+ 'view' => 'View Section Template',
+ 'delete' => 'Delete',
+ 'restore' => 'Restore',
+ 'force_delete' => 'Force Delete',
+
+ // Messages
+ 'created_successfully' => 'Section template created successfully.',
+ 'updated_successfully' => 'Section template updated successfully.',
+ 'deleted_successfully' => 'Section template deleted successfully.',
+ 'restored_successfully' => 'Section template restored successfully.',
+];
+
diff --git a/lang/en/user-guide.php b/lang/en/user-guide.php
new file mode 100644
index 0000000..1427ab0
--- /dev/null
+++ b/lang/en/user-guide.php
@@ -0,0 +1,15 @@
+ 'User Guide',
+ 'navigation_label' => 'User Guide',
+
+ 'menu_title' => 'Guides',
+ 'search_placeholder' => 'Search guides...',
+ 'no_guides' => 'No guide files found yet.',
+ 'no_guide_selected' => 'No Guide Selected',
+ 'select_guide_from_menu' => 'Please select a guide from the left menu.',
+ 'last_updated' => 'Last updated',
+ 'no_results' => 'No results found.',
+];
+
diff --git a/lang/tr/blog.php b/lang/tr/blog.php
index a16d84f..14a5d05 100644
--- a/lang/tr/blog.php
+++ b/lang/tr/blog.php
@@ -1,6 +1,13 @@
'Blog',
+ 'meta-index-title' => 'Blog',
+ 'meta-index-description' => 'Güncel paylaşımlarımız',
+ 'empty' => 'Şu anda blog yazısı bulunmuyor.',
+
+ // Module labels
'title' => 'Blog Yazıları',
'navigation_label' => 'Blog',
'model_label' => 'Blog Yazısı',
@@ -9,6 +16,8 @@ return [
// Actions
'create' => 'Yeni Blog Yazısı Oluştur',
'edit' => 'Blog Yazısını Düzenle',
+ 'save' => 'Kaydet',
+ 'cancel' => 'İptal',
'delete' => 'Blog Yazısını Sil',
'restore' => 'Blog Yazısını Geri Yükle',
'force_delete' => 'Kalıcı Olarak Sil',
diff --git a/lang/tr/footer-templates.php b/lang/tr/footer-templates.php
new file mode 100644
index 0000000..1877a84
--- /dev/null
+++ b/lang/tr/footer-templates.php
@@ -0,0 +1,43 @@
+ 'Şablonlar',
+ 'navigation_label' => 'Footer Şablonları',
+ 'model_label' => 'Footer Şablonu',
+ 'plural_model_label' => 'Footer Şablonları',
+
+ // Sections
+ 'section_general' => 'Genel Bilgiler',
+ 'section_default_data' => 'Varsayılan Değerler',
+ 'section_default_data_desc' => 'Template oluştururken placeholder\'lar için varsayılan değerleri girebilirsiniz. Bu değerler sayfalarda template seçildiğinde otomatik yüklenecektir.',
+
+ // Form Fields
+ 'field_title' => 'Başlık',
+ 'field_html_content' => 'HTML İçerik',
+ 'field_html_content_help' => 'Placeholder format: {tip.alan_adi} örn: {text.copyright}, {richtext.links}, {email.contact}',
+ 'field_is_active' => 'Aktif',
+
+ // Table Columns
+ 'column_title' => 'Başlık',
+ 'column_is_active' => 'Aktif',
+ 'column_pages_count' => 'Sayfa Sayısı',
+ 'column_created_at' => 'Oluşturulma',
+ 'column_updated_at' => 'Güncellenme',
+ 'column_deleted_at' => 'Silinme',
+
+ // Actions
+ 'create' => 'Yeni Footer Şablonu',
+ 'edit' => 'Footer Şablonunu Düzenle',
+ 'view' => 'Footer Şablonunu Görüntüle',
+ 'delete' => 'Sil',
+ 'restore' => 'Geri Yükle',
+ 'force_delete' => 'Kalıcı Sil',
+
+ // Messages
+ 'created_successfully' => 'Footer şablonu başarıyla oluşturuldu.',
+ 'updated_successfully' => 'Footer şablonu başarıyla güncellendi.',
+ 'deleted_successfully' => 'Footer şablonu başarıyla silindi.',
+ 'restored_successfully' => 'Footer şablonu başarıyla geri yüklendi.',
+];
+
diff --git a/lang/tr/header-templates.php b/lang/tr/header-templates.php
new file mode 100644
index 0000000..5634a31
--- /dev/null
+++ b/lang/tr/header-templates.php
@@ -0,0 +1,43 @@
+ 'Şablonlar',
+ 'navigation_label' => 'Header Şablonları',
+ 'model_label' => 'Header Şablonu',
+ 'plural_model_label' => 'Header Şablonları',
+
+ // Sections
+ 'section_general' => 'Genel Bilgiler',
+ 'section_default_data' => 'Varsayılan Değerler',
+ 'section_default_data_desc' => 'Template oluştururken placeholder\'lar için varsayılan değerleri girebilirsiniz. Bu değerler sayfalarda template seçildiğinde otomatik yüklenecektir.',
+
+ // Form Fields
+ 'field_title' => 'Başlık',
+ 'field_html_content' => 'HTML İçerik',
+ 'field_html_content_help' => 'Placeholder format: {tip.alan_adi} örn: {text.company_name}, {image.logo}, {email.contact}',
+ 'field_is_active' => 'Aktif',
+
+ // Table Columns
+ 'column_title' => 'Başlık',
+ 'column_is_active' => 'Aktif',
+ 'column_pages_count' => 'Sayfa Sayısı',
+ 'column_created_at' => 'Oluşturulma',
+ 'column_updated_at' => 'Güncellenme',
+ 'column_deleted_at' => 'Silinme',
+
+ // Actions
+ 'create' => 'Yeni Header Şablonu',
+ 'edit' => 'Header Şablonunu Düzenle',
+ 'view' => 'Header Şablonunu Görüntüle',
+ 'delete' => 'Sil',
+ 'restore' => 'Geri Yükle',
+ 'force_delete' => 'Kalıcı Sil',
+
+ // Messages
+ 'created_successfully' => 'Header şablonu başarıyla oluşturuldu.',
+ 'updated_successfully' => 'Header şablonu başarıyla güncellendi.',
+ 'deleted_successfully' => 'Header şablonu başarıyla silindi.',
+ 'restored_successfully' => 'Header şablonu başarıyla geri yüklendi.',
+];
+
diff --git a/lang/tr/language.php b/lang/tr/language.php
index debca43..7a470b3 100644
--- a/lang/tr/language.php
+++ b/lang/tr/language.php
@@ -10,6 +10,8 @@ return [
// Actions
'create' => 'Dil Oluştur',
'edit' => 'Dil Düzenle',
+ 'save' => 'Kaydet',
+ 'cancel' => 'İptal',
'delete' => 'Dil Sil',
'restore' => 'Geri Yükle',
'force_delete' => 'Kalıcı Olarak Sil',
diff --git a/lang/tr/menu-templates.php b/lang/tr/menu-templates.php
new file mode 100644
index 0000000..1f36fb5
--- /dev/null
+++ b/lang/tr/menu-templates.php
@@ -0,0 +1,42 @@
+ 'Şablonlar',
+ 'navigation_label' => 'Menü Şablonları',
+ 'model_label' => 'Menü Şablonu',
+ 'plural_model_label' => 'Menü Şablonları',
+
+ // Sections
+ 'section_general' => 'Genel Bilgiler',
+ 'section_structure' => 'Menü Yapısı',
+ 'section_structure_desc' => 'Menü yapısını sürükle-bırak ile düzenleyebilir veya JSON formatında kod olarak yazabilirsiniz.',
+
+ // Form Fields
+ 'field_title' => 'Başlık',
+ 'field_html_content' => 'Menü HTML Kodu',
+ 'field_html_content_help' => 'Menü yapısını {menu} placeholder\'ı ile HTML içine ekleyebilirsiniz. Menü otomatik olarak sayfalar tabanlı rendering ile oluşturulur.',
+ 'field_is_active' => 'Aktif',
+
+ // Table Columns
+ 'column_title' => 'Başlık',
+ 'column_is_active' => 'Aktif',
+ 'column_created_at' => 'Oluşturulma',
+ 'column_updated_at' => 'Güncellenme',
+ 'column_deleted_at' => 'Silinme',
+
+ // Actions
+ 'create' => 'Yeni Menü Şablonu',
+ 'edit' => 'Menü Şablonunu Düzenle',
+ 'view' => 'Menü Şablonunu Görüntüle',
+ 'delete' => 'Sil',
+ 'restore' => 'Geri Yükle',
+ 'force_delete' => 'Kalıcı Sil',
+
+ // Messages
+ 'created_successfully' => 'Menü şablonu başarıyla oluşturuldu.',
+ 'updated_successfully' => 'Menü şablonu başarıyla güncellendi.',
+ 'deleted_successfully' => 'Menü şablonu başarıyla silindi.',
+ 'restored_successfully' => 'Menü şablonu başarıyla geri yüklendi.',
+];
+
diff --git a/lang/tr/pages.php b/lang/tr/pages.php
index 2b020aa..286d674 100644
--- a/lang/tr/pages.php
+++ b/lang/tr/pages.php
@@ -1,6 +1,13 @@
'Anasayfa',
+ 'nav-about' => 'Hakkımızda',
+ 'nav-services' => 'Hizmetler',
+ 'nav-contact' => 'İletişim',
+
+ // Module labels
'title' => 'Sayfalar',
'navigation_label' => 'Sayfalar',
'model_label' => 'Sayfa',
@@ -9,28 +16,29 @@ return [
// Actions
'create' => 'Yeni Sayfa Oluştur',
'edit' => 'Sayfayı Düzenle',
+ 'view' => 'Görüntüle',
+ 'save' => 'Kaydet',
+ 'cancel' => 'İptal',
'delete' => 'Sayfayı Sil',
'restore' => 'Sayfayı Geri Yükle',
'force_delete' => 'Kalıcı Olarak Sil',
- // Form fields
- 'title_field' => 'Başlık',
- 'slug_field' => 'URL Yolu',
- 'content_field' => 'İçerik',
- 'meta_title_field' => 'Meta Başlık',
- 'meta_description_field' => 'Meta Açıklama',
- 'status_field' => 'Durum',
- 'published_at_field' => 'Yayın Tarihi',
+ // Menu Tree
+ 'menu_tree_title' => 'Menü Yapısı',
+ 'menu_refresh' => 'Yenile',
+ 'menu_back_to_pages' => 'Sayfalara Dön',
+ 'menu_tree_saved' => 'Menü yapısı başarıyla kaydedildi.',
// Form sections
'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
'title_field' => 'Başlık',
- 'slug_field' => 'URL Slug',
+ 'slug_field' => 'URL Yolu',
'content_field' => 'İçerik',
'excerpt_field' => 'Özet',
'featured_image_field' => 'Öne Çıkan Görsel',
@@ -97,4 +105,69 @@ 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',
+
+ // Dynamic Template System
+ 'form_section_header_template' => 'Header Şablonu',
+ 'form_section_header_template_desc' => 'Sayfanın üst kısmı için dinamik header şablonu seçin',
+ 'header_template_field' => 'Header Şablonu',
+
+ 'form_section_template_sections' => 'Şablon Bölümleri',
+ 'form_section_template_sections_desc' => 'Sayfa için dinamik şablon bölümleri ekleyin',
+ 'template_sections_field' => 'Şablon Bölümleri',
+ 'section_template_field' => 'Bölüm Şablonu',
+ 'add_template_section' => 'Bölüm Ekle',
+ 'template_section' => 'Şablon Bölümü',
+
+ 'form_section_footer_template' => 'Footer Şablonu',
+ 'form_section_footer_template_desc' => 'Sayfanın alt kısmı için dinamik footer şablonu seçin',
+ 'footer_template_field' => 'Footer Şablonu',
];
diff --git a/lang/tr/placeholder-picker.php b/lang/tr/placeholder-picker.php
new file mode 100644
index 0000000..a3085ab
--- /dev/null
+++ b/lang/tr/placeholder-picker.php
@@ -0,0 +1,37 @@
+ 'Placeholder Değişkenleri',
+ 'click_to_insert' => 'Tıklayarak editöre ekle',
+ 'special_placeholders' => 'Özel Placeholder\'lar',
+ 'custom_placeholders' => 'Özel Placeholder\'lar',
+ 'search_placeholder' => 'Placeholder ara...',
+ 'no_results' => 'Arama sonucu bulunamadı',
+
+ // Placeholder Tipleri
+ 'type_text' => 'Metin',
+ 'type_email' => 'E-posta',
+ 'type_url' => 'URL',
+ 'type_tel' => 'Telefon',
+ 'type_number' => 'Sayı',
+ 'type_textarea' => 'Çok Satırlı Metin',
+ 'type_richtext' => 'Zengin Metin',
+ 'type_markdown' => 'Markdown',
+ 'type_code' => 'Kod',
+ 'type_date' => 'Tarih',
+ 'type_datetime' => 'Tarih ve Saat',
+ 'type_time' => 'Saat',
+ 'type_image' => 'Resim',
+ 'type_images' => 'Resimler (Çoklu)',
+ 'type_file' => 'Dosya',
+ 'type_files' => 'Dosyalar (Çoklu)',
+ 'type_select' => 'Seçim',
+ 'type_multiselect' => 'Çoklu Seçim',
+ 'type_checkbox' => 'Onay Kutusu',
+ 'type_radio' => 'Radyo Butonu',
+ 'type_toggle' => 'Aç/Kapa',
+ 'type_color' => 'Renk',
+ 'type_tags' => 'Etiketler',
+ 'type_custom' => 'Özel',
+];
+
diff --git a/lang/tr/section-templates.php b/lang/tr/section-templates.php
new file mode 100644
index 0000000..acc6a24
--- /dev/null
+++ b/lang/tr/section-templates.php
@@ -0,0 +1,42 @@
+ 'Şablonlar',
+ 'navigation_label' => 'Section Şablonları',
+ 'model_label' => 'Section Şablonu',
+ 'plural_model_label' => 'Section Şablonları',
+
+ // Sections
+ 'section_general' => 'Genel Bilgiler',
+ 'section_default_data' => 'Varsayılan Değerler',
+ 'section_default_data_desc' => 'Template oluştururken placeholder\'lar için varsayılan değerleri girebilirsiniz. Bu değerler sayfalarda template seçildiğinde otomatik yüklenecektir.',
+
+ // Form Fields
+ 'field_title' => 'Başlık',
+ 'field_html_content' => 'HTML İçerik',
+ 'field_html_content_help' => 'Placeholder format: {tip.alan_adi} örn: {text.title}, {richtext.content}, {image.banner}',
+ 'field_is_active' => 'Aktif',
+
+ // Table Columns
+ 'column_title' => 'Başlık',
+ 'column_is_active' => 'Aktif',
+ 'column_created_at' => 'Oluşturulma',
+ 'column_updated_at' => 'Güncellenme',
+ 'column_deleted_at' => 'Silinme',
+
+ // Actions
+ 'create' => 'Yeni Section Şablonu',
+ 'edit' => 'Section Şablonunu Düzenle',
+ 'view' => 'Section Şablonunu Görüntüle',
+ 'delete' => 'Sil',
+ 'restore' => 'Geri Yükle',
+ 'force_delete' => 'Kalıcı Sil',
+
+ // Messages
+ 'created_successfully' => 'Section şablonu başarıyla oluşturuldu.',
+ 'updated_successfully' => 'Section şablonu başarıyla güncellendi.',
+ 'deleted_successfully' => 'Section şablonu başarıyla silindi.',
+ 'restored_successfully' => 'Section şablonu başarıyla geri yüklendi.',
+];
+
diff --git a/lang/tr/user-guide.php b/lang/tr/user-guide.php
new file mode 100644
index 0000000..027a86e
--- /dev/null
+++ b/lang/tr/user-guide.php
@@ -0,0 +1,15 @@
+ 'Kullanıcı Kılavuzu',
+ 'navigation_label' => 'Kullanıcı Kılavuzu',
+
+ 'menu_title' => 'Kılavuzlar',
+ 'search_placeholder' => 'Kılavuz ara...',
+ 'no_guides' => 'Henüz kılavuz dosyası bulunmamaktadır.',
+ 'no_guide_selected' => 'Kılavuz Seçilmedi',
+ 'select_guide_from_menu' => 'Lütfen sol menüden bir kılavuz seçin.',
+ 'last_updated' => 'Son güncelleme',
+ 'no_results' => 'Arama sonucu bulunamadı.',
+];
+
diff --git a/public/css/solution-forest/filament-tree/filament-tree-min.css b/public/css/solution-forest/filament-tree/filament-tree-min.css
new file mode 100644
index 0000000..31ed5fb
--- /dev/null
+++ b/public/css/solution-forest/filament-tree/filament-tree-min.css
@@ -0,0 +1,2 @@
+/*! tailwindcss v4.1.13 | MIT License | https://tailwindcss.com */
+@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-gray-50:oklch(98.5% .002 247.839);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-950:oklch(13% .028 261.692);--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--font-weight-medium:500;--radius-lg:.5rem;--radius-xl:.75rem;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.mb-2{margin-bottom:calc(var(--spacing)*2)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-auto{margin-left:auto}.flex{display:flex}.hidden{display:none}.h-4{height:calc(var(--spacing)*4)}.w-4{width:calc(var(--spacing)*4)}.w-full{width:100%}.cursor-wait{cursor:wait}.items-center{align-items:center}.gap-1{gap:calc(var(--spacing)*1)}.gap-2{gap:calc(var(--spacing)*2)}.overflow-hidden{overflow:hidden}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-300{border-color:var(--color-gray-300)}.bg-white{background-color:var(--color-white)}.px-5{padding-inline:calc(var(--spacing)*5)}.py-2{padding-block:calc(var(--spacing)*2)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.opacity-70{opacity:.7}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media (prefers-color-scheme:dark){.dark\:border-gray-600{border-color:var(--color-gray-600)}.dark\:bg-gray-500\/10{background-color:#6a72821a}@supports (color:color-mix(in lab, red, red)){.dark\:bg-gray-500\/10{background-color:color-mix(in oklab,var(--color-gray-500)10%,transparent)}}}}.dd{max-width:600px;margin:0;padding:0;font-size:13px;line-height:20px;list-style:none;display:block;position:relative}.dd-list{margin:0;padding:0;list-style:none;display:block;position:relative}.dd-list .dd-list{padding-left:30px}[dir=rtl] .dd-list .dd-list{padding-left:unset!important;padding-right:30px!important}.dd-empty,.dd-item,.dd-placeholder{min-height:20px;margin:0;padding:0;font-size:13px;line-height:20px;display:block;position:relative}.dd-item>button{cursor:pointer;float:left;white-space:nowrap;text-align:center;background:0 0;border:0;width:25px;height:20px;margin:5px 0;padding:0;font-size:12px;font-weight:700;line-height:1;position:relative;overflow:hidden}[dir=rtl] .dd-item>button{float:right}.dd-item>button:before{text-align:center;text-indent:0;width:100%;display:block;position:absolute}.dd-item>button.dd-expand:before{content:"+"}.dd-item>button.dd-collapse:before{content:"-"}.dd-expand,.dd-collapsed .dd-collapse,.dd-collapsed .dd-list{display:none}.dd-collapsed .dd-expand{display:block}.dd-empty,.dd-placeholder{box-sizing:border-box;background:#f2fbff;border:1px dashed #b6bcbf;min-height:30px;margin:5px 0;padding:0}:is(.dd-empty,.dd-placeholder):is(.dark *){background:#1f2937;border-color:#4b5563}.dd-empty{background-color:#e5e5e5;background-position:0 0,30px 30px;background-size:60px 60px;border:1px dashed #bbb;min-height:100px}.dd-dragel{pointer-events:none;z-index:9999;position:absolute}.dd-dragel>.dd-item .dd-handle{margin-top:0}.dd-dragel .dd-handle{background-color:#fffc;height:40px;padding:4px;box-shadow:2px 4px 6px #0000001a}.dd-dragel .dd-handle:is(.dark *){background-color:#1f2937cc}.dd-nochildren .dd-placeholder{display:none}.dd{max-width:initial}.dd-handle{height:55px}.dd-item>button{height:45px}.btn-group{gap:1px;display:flex}.btn-group button{border-radius:0}[dir=ltr] .btn-group button:first-of-type{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}[dir=ltr] .btn-group button:last-of-type,[dir=rtl] .btn-group button:first-of-type{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}[dir=rtl] .btn-group button:last-of-type{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.btn-group button:first-of-type{border-radius:.5rem 0 0 .5rem}[dir=rtl] .btn-group button:first-of-type,.btn-group button:last-of-type{border-radius:0 .5rem .5rem 0}[dir=rtl] .btn-group button:last-of-type{border-radius:.5rem 0 0 .5rem}.btn-group button:only-of-type{border-radius:.5rem!important}.filament-tree .dd-item .dd-handle,.filament-tree .dd-item>.loading-indicator,.dd-dragel .dd-handle{margin-bottom:calc(var(--spacing)*2);height:calc(var(--spacing)*10);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300)}:is(.filament-tree .dd-item .dd-handle,.filament-tree .dd-item>.loading-indicator,.dd-dragel .dd-handle):is(.dark *){border-color:var(--color-gray-700)}.filament-tree .dd-placeholder{align-items:center;width:100%;display:flex}.filament-tree .dd-item .dd-handle{background-color:#fff;align-items:center;width:100%;display:flex}.filament-tree .dd-item .dd-handle:is(.dark *){background-color:var(--color-gray-950)}.filament-tree .dd-item .dd-handle>button{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg);border-right-style:var(--tw-border-style);border-right-width:1px;align-items:center;height:100%;padding-inline:1px;display:flex}.filament-tree .dd-item .dd-handle>button:where(:dir(rtl),[dir=rtl],[dir=rtl] *){border-right-style:var(--tw-border-style);border-right-width:0;border-left-style:var(--tw-border-style);border-left-width:1px;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.filament-tree .dd-item .dd-handle>button{background-color:var(--color-gray-50);border-color:var(--color-gray-300)}.filament-tree .dd-item .dd-handle>button:is(.dark *){background-color:var(--color-gray-700);border-color:var(--color-gray-700)}.filament-tree .dd-item .dd-handle>button svg{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}.filament-tree .dd-item .dd-handle>button svg:first-child{margin-right:calc(var(--spacing)*-2)}.filament-tree .dd-item .dd-handle>button svg:first-child:where(:dir(rtl),[dir=rtl],[dir=rtl] *){margin-right:calc(var(--spacing)*0);margin-left:calc(var(--spacing)*-2)}.filament-tree .dd-item .dd-handle>button svg{color:var(--color-gray-400)}.filament-tree .dd-item .dd-handle>button svg:is(.dark *){color:#fff}.dd-dragel .dd-content{height:100%}.dd-dragel .dd-handle>button,.dd-dragel .fi-tree-actions-ctn{display:none}.filament-tree .dd-item .dd-content,.dd-dragel .dd-content{gap:calc(var(--spacing)*1);display:flex}.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display{margin-left:calc(var(--spacing)*1);align-items:center;gap:calc(var(--spacing)*1);flex:1;display:flex}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display):where(:dir(rtl),[dir=rtl],[dir=rtl] *){margin-right:calc(var(--spacing)*1)}.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display{width:100%;max-width:150px}@media (min-width:48rem){.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display{max-width:155px}}@media (min-width:64rem){.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display{max-width:200px}}@media (min-width:80rem){.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display{max-width:350px}}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display) .icon-ctn svg{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4)}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display) .item-content-ctn{min-width:calc(var(--spacing)*0);max-width:var(--container-xs);flex-direction:column;flex:1;display:flex}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display) .item-content-ctn .item-title{text-overflow:ellipsis;white-space:nowrap;--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);overflow:hidden}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display) .item-content-ctn .item-description{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500);overflow:hidden}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display) .item-content-ctn .item-description:is(.dark *){color:var(--color-gray-400)}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display):not(:has(.icon-ctn)) .item-content-ctn{margin-left:calc(var(--spacing)*4)}:is(.filament-tree .dd-item .dd-content>.tree-item-display,.dd-dragel .tree-item-display):not(:has(.icon-ctn)) .item-content-ctn:where(:dir(rtl),[dir=rtl],[dir=rtl] *){margin-right:calc(var(--spacing)*4)}.filament-tree .dd-item .dd-content .dd-item-btns{flex:1;justify-content:center;align-items:center;display:flex}.filament-tree .dd-item .dd-content .dd-item-btns button svg{height:calc(var(--spacing)*4);width:calc(var(--spacing)*4);color:var(--color-gray-400)}.filament-tree .dd-item:not(:has(.dd-list)) .dd-item-btns{display:none}.filament-tree .dd-item .fi-tree-actions-ctn{margin-right:calc(var(--spacing)*4);margin-left:auto}.filament-tree .dd-item .fi-tree-actions-ctn:where(:dir(rtl),[dir=rtl],[dir=rtl] *){margin-right:auto;margin-left:calc(var(--spacing)*4)}.filament-tree .dd-item .fi-tree-actions-ctn .fi-tree-actions{align-items:center;gap:calc(var(--spacing)*3);flex-shrink:0;display:flex}.filament-tree .dd-item>.loading-indicator{animation:var(--animate-pulse);background-color:var(--color-gray-300);display:none}.filament-tree .dd-item>.loading-indicator:is(.dark *){background-color:var(--color-gray-600)}.filament-tree-component .nestable-menu{margin-bottom:calc(var(--spacing)*4)}.filament-tree-component .nestable-menu,.filament-tree-component .nestable-menu .toolbar-btns{align-items:center;gap:calc(var(--spacing)*2);display:flex}.filament-tree-component .nestable-menu{justify-content:space-between}.filament-tree-component .nestable-menu .toolbar-btns.main{flex:1}.filament-tree-list.hidden{display:none}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@keyframes pulse{50%{opacity:.5}}
\ No newline at end of file
diff --git a/public/js/solution-forest/filament-tree/components/filament-tree-component.js b/public/js/solution-forest/filament-tree/components/filament-tree-component.js
new file mode 100644
index 0000000..759b433
--- /dev/null
+++ b/public/js/solution-forest/filament-tree/components/filament-tree-component.js
@@ -0,0 +1,25 @@
+var Gi=Object.create;var On=Object.defineProperty;var Qi=Object.getOwnPropertyDescriptor;var Ji=Object.getOwnPropertyNames;var Ki=Object.getPrototypeOf,Zi=Object.prototype.hasOwnProperty;var er=(h,N)=>()=>(N||h((N={exports:{}}).exports,N),N.exports);var tr=(h,N,S,Y)=>{if(N&&typeof N=="object"||typeof N=="function")for(let k of Ji(N))!Zi.call(h,k)&&k!==S&&On(h,k,{get:()=>N[k],enumerable:!(Y=Qi(N,k))||Y.enumerable});return h};var Mn=(h,N,S)=>(S=h!=null?Gi(Ki(h)):{},tr(N||!h||!h.__esModule?On(S,"default",{value:h,enumerable:!0}):S,h));var at=er((Rn,bt)=>{(function(h,N){"use strict";typeof bt=="object"&&typeof bt.exports=="object"?bt.exports=h.document?N(h,!0):function(S){if(!S.document)throw new Error("jQuery requires a window with a document");return N(S)}:N(h)})(typeof window<"u"?window:Rn,function(h,N){"use strict";var S=[],Y=Object.getPrototypeOf,k=S.slice,W=S.flat?function(e){return S.flat.call(e)}:function(e){return S.concat.apply([],e)},I=S.push,E=S.indexOf,ce={},Ce=ce.toString,ae=ce.hasOwnProperty,Me=ae.toString,xt=Me.call(Object),O={},M=function(t){return typeof t=="function"&&typeof t.nodeType!="number"&&typeof t.item!="function"},Re=function(t){return t!=null&&t===t.window},P=h.document,Wn={type:!0,src:!0,nonce:!0,noModule:!0};function zt(e,t,n){n=n||P;var i,o,s=n.createElement("script");if(s.text=e,t)for(i in Wn)o=t[i]||t.getAttribute&&t.getAttribute(i),o&&s.setAttribute(i,o);n.head.appendChild(s).parentNode.removeChild(s)}function Ie(e){return e==null?e+"":typeof e=="object"||typeof e=="function"?ce[Ce.call(e)]||"object":typeof e}var Ut="3.7.1",Fn=/HTML$/i,r=function(e,t){return new r.fn.init(e,t)};r.fn=r.prototype={jquery:Ut,constructor:r,length:0,toArray:function(){return k.call(this)},get:function(e){return e==null?k.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=r.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return r.each(this,e)},map:function(e){return this.pushStack(r.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(k.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(r.grep(this,function(e,t){return(t+1)%2}))},odd:function(){return this.pushStack(r.grep(this,function(e,t){return t%2}))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e}function Q(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var Yn=S.pop,Bn=S.sort,$n=S.splice,U="[\\x20\\t\\r\\n\\f]",Ge=new RegExp("^"+U+"+|((?:^|[^\\\\])(?:\\\\.)*)"+U+"+$","g");r.contains=function(e,t){var n=t&&t.parentNode;return e===n||!!(n&&n.nodeType===1&&(e.contains?e.contains(n):e.compareDocumentPosition&&e.compareDocumentPosition(n)&16))};var zn=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;function Un(e,t){return t?e==="\0"?"\uFFFD":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e}r.escapeSelector=function(e){return(e+"").replace(zn,Un)};var Te=P,Tt=I;(function(){var e,t,n,i,o,s=Tt,a,l,f,d,v,b=r.expando,g=0,x=0,H=gt(),B=gt(),R=gt(),Z=gt(),K=function(u,c){return u===c&&(o=!0),0},ye="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",ve="(?:\\\\[\\da-fA-F]{1,6}"+U+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",F="\\["+U+"*("+ve+")(?:"+U+"*([*^$|!~]?=)"+U+`*(?:'((?:\\\\.|[^\\\\'])*)'|"((?:\\\\.|[^\\\\"])*)"|(`+ve+"))|)"+U+"*\\]",Pe=":("+ve+`)(?:\\((('((?:\\\\.|[^\\\\'])*)'|"((?:\\\\.|[^\\\\"])*)")|((?:\\\\.|[^\\\\()[\\]]|`+F+")*)|.*)\\)|)",$=new RegExp(U+"+","g"),J=new RegExp("^"+U+"*,"+U+"*"),rt=new RegExp("^"+U+"*([>+~]|"+U+")"+U+"*"),Xt=new RegExp(U+"|>"),me=new RegExp(Pe),ot=new RegExp("^"+ve+"$"),be={ID:new RegExp("^#("+ve+")"),CLASS:new RegExp("^\\.("+ve+")"),TAG:new RegExp("^("+ve+"|[*])"),ATTR:new RegExp("^"+F),PSEUDO:new RegExp("^"+Pe),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+U+"*(even|odd|(([+-]|)(\\d*)n|)"+U+"*(?:([+-]|)"+U+"*(\\d+)|))"+U+"*\\)|)","i"),bool:new RegExp("^(?:"+ye+")$","i"),needsContext:new RegExp("^"+U+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+U+"*((?:-\\d)?\\d*)"+U+"*\\)|)(?=[^-]|$)","i")},De=/^(?:input|select|textarea|button)$/i,ke=/^h\d$/i,fe=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_t=/[+~]/,Ne=new RegExp("\\\\[\\da-fA-F]{1,6}"+U+"?|\\\\([^\\r\\n\\f])","g"),Se=function(u,c){var p="0x"+u.slice(1)-65536;return c||(p<0?String.fromCharCode(p+65536):String.fromCharCode(p>>10|55296,p&1023|56320))},Fi=function(){je()},Yi=vt(function(u){return u.disabled===!0&&Q(u,"fieldset")},{dir:"parentNode",next:"legend"});function Bi(){try{return a.activeElement}catch{}}try{s.apply(S=k.call(Te.childNodes),Te.childNodes),S[Te.childNodes.length].nodeType}catch{s={apply:function(c,p){Tt.apply(c,k.call(p))},call:function(c){Tt.apply(c,k.call(arguments,1))}}}function z(u,c,p,y){var m,C,T,A,w,X,L,q=c&&c.ownerDocument,_=c?c.nodeType:9;if(p=p||[],typeof u!="string"||!u||_!==1&&_!==9&&_!==11)return p;if(!y&&(je(c),c=c||a,f)){if(_!==11&&(w=fe.exec(u)))if(m=w[1]){if(_===9)if(T=c.getElementById(m)){if(T.id===m)return s.call(p,T),p}else return p;else if(q&&(T=q.getElementById(m))&&z.contains(c,T)&&T.id===m)return s.call(p,T),p}else{if(w[2])return s.apply(p,c.getElementsByTagName(u)),p;if((m=w[3])&&c.getElementsByClassName)return s.apply(p,c.getElementsByClassName(m)),p}if(!Z[u+" "]&&(!d||!d.test(u))){if(L=u,q=c,_===1&&(Xt.test(u)||rt.test(u))){for(q=_t.test(u)&&Wt(c.parentNode)||c,(q!=c||!O.scope)&&((A=c.getAttribute("id"))?A=r.escapeSelector(A):c.setAttribute("id",A=b)),X=st(u),C=X.length;C--;)X[C]=(A?"#"+A:":scope")+" "+yt(X[C]);L=X.join(",")}try{return s.apply(p,q.querySelectorAll(L)),p}catch{Z(u,!0)}finally{A===b&&c.removeAttribute("id")}}}return Pn(u.replace(Ge,"$1"),c,p,y)}function gt(){var u=[];function c(p,y){return u.push(p+" ")>t.cacheLength&&delete c[u.shift()],c[p+" "]=y}return c}function pe(u){return u[b]=!0,u}function Ue(u){var c=a.createElement("fieldset");try{return!!u(c)}catch{return!1}finally{c.parentNode&&c.parentNode.removeChild(c),c=null}}function $i(u){return function(c){return Q(c,"input")&&c.type===u}}function zi(u){return function(c){return(Q(c,"input")||Q(c,"button"))&&c.type===u}}function Hn(u){return function(c){return"form"in c?c.parentNode&&c.disabled===!1?"label"in c?"label"in c.parentNode?c.parentNode.disabled===u:c.disabled===u:c.isDisabled===u||c.isDisabled!==!u&&Yi(c)===u:c.disabled===u:"label"in c?c.disabled===u:!1}}function Oe(u){return pe(function(c){return c=+c,pe(function(p,y){for(var m,C=u([],p.length,c),T=C.length;T--;)p[m=C[T]]&&(p[m]=!(y[m]=p[m]))})})}function Wt(u){return u&&typeof u.getElementsByTagName<"u"&&u}function je(u){var c,p=u?u.ownerDocument||u:Te;return p==a||p.nodeType!==9||!p.documentElement||(a=p,l=a.documentElement,f=!r.isXMLDoc(a),v=l.matches||l.webkitMatchesSelector||l.msMatchesSelector,l.msMatchesSelector&&Te!=a&&(c=a.defaultView)&&c.top!==c&&c.addEventListener("unload",Fi),O.getById=Ue(function(y){return l.appendChild(y).id=r.expando,!a.getElementsByName||!a.getElementsByName(r.expando).length}),O.disconnectedMatch=Ue(function(y){return v.call(y,"*")}),O.scope=Ue(function(){return a.querySelectorAll(":scope")}),O.cssHas=Ue(function(){try{return a.querySelector(":has(*,:jqfake)"),!1}catch{return!0}}),O.getById?(t.filter.ID=function(y){var m=y.replace(Ne,Se);return function(C){return C.getAttribute("id")===m}},t.find.ID=function(y,m){if(typeof m.getElementById<"u"&&f){var C=m.getElementById(y);return C?[C]:[]}}):(t.filter.ID=function(y){var m=y.replace(Ne,Se);return function(C){var T=typeof C.getAttributeNode<"u"&&C.getAttributeNode("id");return T&&T.value===m}},t.find.ID=function(y,m){if(typeof m.getElementById<"u"&&f){var C,T,A,w=m.getElementById(y);if(w){if(C=w.getAttributeNode("id"),C&&C.value===y)return[w];for(A=m.getElementsByName(y),T=0;w=A[T++];)if(C=w.getAttributeNode("id"),C&&C.value===y)return[w]}return[]}}),t.find.TAG=function(y,m){return typeof m.getElementsByTagName<"u"?m.getElementsByTagName(y):m.querySelectorAll(y)},t.find.CLASS=function(y,m){if(typeof m.getElementsByClassName<"u"&&f)return m.getElementsByClassName(y)},d=[],Ue(function(y){var m;l.appendChild(y).innerHTML="",y.querySelectorAll("[selected]").length||d.push("\\["+U+"*(?:value|"+ye+")"),y.querySelectorAll("[id~="+b+"-]").length||d.push("~="),y.querySelectorAll("a#"+b+"+*").length||d.push(".#.+[+~]"),y.querySelectorAll(":checked").length||d.push(":checked"),m=a.createElement("input"),m.setAttribute("type","hidden"),y.appendChild(m).setAttribute("name","D"),l.appendChild(y).disabled=!0,y.querySelectorAll(":disabled").length!==2&&d.push(":enabled",":disabled"),m=a.createElement("input"),m.setAttribute("name",""),y.appendChild(m),y.querySelectorAll("[name='']").length||d.push("\\["+U+"*name"+U+"*="+U+`*(?:''|"")`)}),O.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),K=function(y,m){if(y===m)return o=!0,0;var C=!y.compareDocumentPosition-!m.compareDocumentPosition;return C||(C=(y.ownerDocument||y)==(m.ownerDocument||m)?y.compareDocumentPosition(m):1,C&1||!O.sortDetached&&m.compareDocumentPosition(y)===C?y===a||y.ownerDocument==Te&&z.contains(Te,y)?-1:m===a||m.ownerDocument==Te&&z.contains(Te,m)?1:i?E.call(i,y)-E.call(i,m):0:C&4?-1:1)}),a}z.matches=function(u,c){return z(u,null,null,c)},z.matchesSelector=function(u,c){if(je(u),f&&!Z[c+" "]&&(!d||!d.test(c)))try{var p=v.call(u,c);if(p||O.disconnectedMatch||u.document&&u.document.nodeType!==11)return p}catch{Z(c,!0)}return z(c,a,null,[u]).length>0},z.contains=function(u,c){return(u.ownerDocument||u)!=a&&je(u),r.contains(u,c)},z.attr=function(u,c){(u.ownerDocument||u)!=a&&je(u);var p=t.attrHandle[c.toLowerCase()],y=p&&ae.call(t.attrHandle,c.toLowerCase())?p(u,c,!f):void 0;return y!==void 0?y:u.getAttribute(c)},z.error=function(u){throw new Error("Syntax error, unrecognized expression: "+u)},r.uniqueSort=function(u){var c,p=[],y=0,m=0;if(o=!O.sortStable,i=!O.sortStable&&k.call(u,0),Bn.call(u,K),o){for(;c=u[m++];)c===u[m]&&(y=p.push(m));for(;y--;)$n.call(u,p[y],1)}return i=null,u},r.fn.uniqueSort=function(){return this.pushStack(r.uniqueSort(k.apply(this)))},t=r.expr={cacheLength:50,createPseudo:pe,match:be,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(u){return u[1]=u[1].replace(Ne,Se),u[3]=(u[3]||u[4]||u[5]||"").replace(Ne,Se),u[2]==="~="&&(u[3]=" "+u[3]+" "),u.slice(0,4)},CHILD:function(u){return u[1]=u[1].toLowerCase(),u[1].slice(0,3)==="nth"?(u[3]||z.error(u[0]),u[4]=+(u[4]?u[5]+(u[6]||1):2*(u[3]==="even"||u[3]==="odd")),u[5]=+(u[7]+u[8]||u[3]==="odd")):u[3]&&z.error(u[0]),u},PSEUDO:function(u){var c,p=!u[6]&&u[2];return be.CHILD.test(u[0])?null:(u[3]?u[2]=u[4]||u[5]||"":p&&me.test(p)&&(c=st(p,!0))&&(c=p.indexOf(")",p.length-c)-p.length)&&(u[0]=u[0].slice(0,c),u[2]=p.slice(0,c)),u.slice(0,3))}},filter:{TAG:function(u){var c=u.replace(Ne,Se).toLowerCase();return u==="*"?function(){return!0}:function(p){return Q(p,c)}},CLASS:function(u){var c=H[u+" "];return c||(c=new RegExp("(^|"+U+")"+u+"("+U+"|$)"))&&H(u,function(p){return c.test(typeof p.className=="string"&&p.className||typeof p.getAttribute<"u"&&p.getAttribute("class")||"")})},ATTR:function(u,c,p){return function(y){var m=z.attr(y,u);return m==null?c==="!=":c?(m+="",c==="="?m===p:c==="!="?m!==p:c==="^="?p&&m.indexOf(p)===0:c==="*="?p&&m.indexOf(p)>-1:c==="$="?p&&m.slice(-p.length)===p:c==="~="?(" "+m.replace($," ")+" ").indexOf(p)>-1:c==="|="?m===p||m.slice(0,p.length+1)===p+"-":!1):!0}},CHILD:function(u,c,p,y,m){var C=u.slice(0,3)!=="nth",T=u.slice(-4)!=="last",A=c==="of-type";return y===1&&m===0?function(w){return!!w.parentNode}:function(w,X,L){var q,_,j,V,se,ee=C!==T?"nextSibling":"previousSibling",le=w.parentNode,xe=A&&w.nodeName.toLowerCase(),Ve=!L&&!A,te=!1;if(le){if(C){for(;ee;){for(j=w;j=j[ee];)if(A?Q(j,xe):j.nodeType===1)return!1;se=ee=u==="only"&&!se&&"nextSibling"}return!0}if(se=[T?le.firstChild:le.lastChild],T&&Ve){for(_=le[b]||(le[b]={}),q=_[u]||[],V=q[0]===g&&q[1],te=V&&q[2],j=V&&le.childNodes[V];j=++V&&j&&j[ee]||(te=V=0)||se.pop();)if(j.nodeType===1&&++te&&j===w){_[u]=[g,V,te];break}}else if(Ve&&(_=w[b]||(w[b]={}),q=_[u]||[],V=q[0]===g&&q[1],te=V),te===!1)for(;(j=++V&&j&&j[ee]||(te=V=0)||se.pop())&&!((A?Q(j,xe):j.nodeType===1)&&++te&&(Ve&&(_=j[b]||(j[b]={}),_[u]=[g,te]),j===w)););return te-=m,te===y||te%y===0&&te/y>=0}}},PSEUDO:function(u,c){var p,y=t.pseudos[u]||t.setFilters[u.toLowerCase()]||z.error("unsupported pseudo: "+u);return y[b]?y(c):y.length>1?(p=[u,u,"",c],t.setFilters.hasOwnProperty(u.toLowerCase())?pe(function(m,C){for(var T,A=y(m,c),w=A.length;w--;)T=E.call(m,A[w]),m[T]=!(C[T]=A[w])}):function(m){return y(m,0,p)}):y}},pseudos:{not:pe(function(u){var c=[],p=[],y=$t(u.replace(Ge,"$1"));return y[b]?pe(function(m,C,T,A){for(var w,X=y(m,null,A,[]),L=m.length;L--;)(w=X[L])&&(m[L]=!(C[L]=w))}):function(m,C,T){return c[0]=m,y(c,null,T,p),c[0]=null,!p.pop()}}),has:pe(function(u){return function(c){return z(u,c).length>0}}),contains:pe(function(u){return u=u.replace(Ne,Se),function(c){return(c.textContent||r.text(c)).indexOf(u)>-1}}),lang:pe(function(u){return ot.test(u||"")||z.error("unsupported lang: "+u),u=u.replace(Ne,Se).toLowerCase(),function(c){var p;do if(p=f?c.lang:c.getAttribute("xml:lang")||c.getAttribute("lang"))return p=p.toLowerCase(),p===u||p.indexOf(u+"-")===0;while((c=c.parentNode)&&c.nodeType===1);return!1}}),target:function(u){var c=h.location&&h.location.hash;return c&&c.slice(1)===u.id},root:function(u){return u===l},focus:function(u){return u===Bi()&&a.hasFocus()&&!!(u.type||u.href||~u.tabIndex)},enabled:Hn(!1),disabled:Hn(!0),checked:function(u){return Q(u,"input")&&!!u.checked||Q(u,"option")&&!!u.selected},selected:function(u){return u.parentNode&&u.parentNode.selectedIndex,u.selected===!0},empty:function(u){for(u=u.firstChild;u;u=u.nextSibling)if(u.nodeType<6)return!1;return!0},parent:function(u){return!t.pseudos.empty(u)},header:function(u){return ke.test(u.nodeName)},input:function(u){return De.test(u.nodeName)},button:function(u){return Q(u,"input")&&u.type==="button"||Q(u,"button")},text:function(u){var c;return Q(u,"input")&&u.type==="text"&&((c=u.getAttribute("type"))==null||c.toLowerCase()==="text")},first:Oe(function(){return[0]}),last:Oe(function(u,c){return[c-1]}),eq:Oe(function(u,c,p){return[p<0?p+c:p]}),even:Oe(function(u,c){for(var p=0;pc?y=c:y=p;--y>=0;)u.push(y);return u}),gt:Oe(function(u,c,p){for(var y=p<0?p+c:p;++y1?function(c,p,y){for(var m=u.length;m--;)if(!u[m](c,p,y))return!1;return!0}:u[0]}function Ui(u,c,p){for(var y=0,m=c.length;y-1&&(T[L]=!(A[L]=_))}}else j=mt(j===A?j.splice(ee,j.length):j),m?m(null,A,j,X):s.apply(A,j)})}function Bt(u){for(var c,p,y,m=u.length,C=t.relative[u[0].type],T=C||t.relative[" "],A=C?1:0,w=vt(function(q){return q===c},T,!0),X=vt(function(q){return E.call(c,q)>-1},T,!0),L=[function(q,_,j){var V=!C&&(j||_!=n)||((c=_).nodeType?w(q,_,j):X(q,_,j));return c=null,V}];A1&&Ft(L),A>1&&yt(u.slice(0,A-1).concat({value:u[A-2].type===" "?"*":""})).replace(Ge,"$1"),p,A0,y=u.length>0,m=function(C,T,A,w,X){var L,q,_,j=0,V="0",se=C&&[],ee=[],le=n,xe=C||y&&t.find.TAG("*",X),Ve=g+=le==null?1:Math.random()||.1,te=xe.length;for(X&&(n=T==a||T||X);V!==te&&(L=xe[V])!=null;V++){if(y&&L){for(q=0,!T&&L.ownerDocument!=a&&(je(L),A=!f);_=u[q++];)if(_(L,T||a,A)){s.call(w,L);break}X&&(g=Ve)}p&&((L=!_&&L)&&j--,C&&se.push(L))}if(j+=V,p&&V!==j){for(q=0;_=c[q++];)_(se,ee,T,A);if(C){if(j>0)for(;V--;)se[V]||ee[V]||(ee[V]=Yn.call(w));ee=mt(ee)}s.apply(w,ee),X&&!C&&ee.length>0&&j+c.length>1&&r.uniqueSort(w)}return X&&(g=Ve,n=le),se};return p?pe(m):m}function $t(u,c){var p,y=[],m=[],C=R[u+" "];if(!C){for(c||(c=st(u)),p=c.length;p--;)C=Bt(c[p]),C[b]?y.push(C):m.push(C);C=R(u,Vi(m,y)),C.selector=u}return C}function Pn(u,c,p,y){var m,C,T,A,w,X=typeof u=="function"&&u,L=!y&&st(u=X.selector||u);if(p=p||[],L.length===1){if(C=L[0]=L[0].slice(0),C.length>2&&(T=C[0]).type==="ID"&&c.nodeType===9&&f&&t.relative[C[1].type]){if(c=(t.find.ID(T.matches[0].replace(Ne,Se),c)||[])[0],c)X&&(c=c.parentNode);else return p;u=u.slice(C.shift().value.length)}for(m=be.needsContext.test(u)?0:C.length;m--&&(T=C[m],!t.relative[A=T.type]);)if((w=t.find[A])&&(y=w(T.matches[0].replace(Ne,Se),_t.test(C[0].type)&&Wt(c.parentNode)||c))){if(C.splice(m,1),u=y.length&&yt(C),!u)return s.apply(p,y),p;break}}return(X||$t(u,L))(y,c,!f,p,!c||_t.test(u)&&Wt(c.parentNode)||c),p}O.sortStable=b.split("").sort(K).join("")===b,je(),O.sortDetached=Ue(function(u){return u.compareDocumentPosition(a.createElement("fieldset"))&1}),r.find=z,r.expr[":"]=r.expr.pseudos,r.unique=r.uniqueSort,z.compile=$t,z.select=Pn,z.setDocument=je,z.tokenize=st,z.escape=r.escapeSelector,z.getText=r.text,z.isXML=r.isXMLDoc,z.selectors=r.expr,z.support=r.support,z.uniqueSort=r.uniqueSort})();var Xe=function(e,t,n){for(var i=[],o=n!==void 0;(e=e[t])&&e.nodeType!==9;)if(e.nodeType===1){if(o&&r(e).is(n))break;i.push(e)}return i},Vt=function(e,t){for(var n=[];e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n},Gt=r.expr.match.needsContext,Qt=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function wt(e,t,n){return M(t)?r.grep(e,function(i,o){return!!t.call(i,o,i)!==n}):t.nodeType?r.grep(e,function(i){return i===t!==n}):typeof t!="string"?r.grep(e,function(i){return E.call(t,i)>-1!==n}):r.filter(t,e,n)}r.filter=function(e,t,n){var i=t[0];return n&&(e=":not("+e+")"),t.length===1&&i.nodeType===1?r.find.matchesSelector(i,e)?[i]:[]:r.find.matches(e,r.grep(t,function(o){return o.nodeType===1}))},r.fn.extend({find:function(e){var t,n,i=this.length,o=this;if(typeof e!="string")return this.pushStack(r(e).filter(function(){for(t=0;t1?r.uniqueSort(n):n},filter:function(e){return this.pushStack(wt(this,e||[],!1))},not:function(e){return this.pushStack(wt(this,e||[],!0))},is:function(e){return!!wt(this,typeof e=="string"&&Gt.test(e)?r(e):e||[],!1).length}});var Jt,Vn=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,Gn=r.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||Jt,typeof e=="string")if(e[0]==="<"&&e[e.length-1]===">"&&e.length>=3?i=[null,e,null]:i=Vn.exec(e),i&&(i[1]||!t))if(i[1]){if(t=t instanceof r?t[0]:t,r.merge(this,r.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:P,!0)),Qt.test(i[1])&&r.isPlainObject(t))for(i in t)M(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}else return o=P.getElementById(i[2]),o&&(this[0]=o,this.length=1),this;else return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);else{if(e.nodeType)return this[0]=e,this.length=1,this;if(M(e))return n.ready!==void 0?n.ready(e):e(r)}return r.makeArray(e,this)};Gn.prototype=r.fn,Jt=r(P);var Qn=/^(?:parents|prev(?:Until|All))/,Jn={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(e){var t=r(e,this),n=t.length;return this.filter(function(){for(var i=0;i-1:n.nodeType===1&&r.find.matchesSelector(n,e))){s.push(n);break}}return this.pushStack(s.length>1?r.uniqueSort(s):s)},index:function(e){return e?typeof e=="string"?E.call(r(e),this[0]):E.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(e,t))))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}});function Kt(e,t){for(;(e=e[t])&&e.nodeType!==1;);return e}r.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return Xe(e,"parentNode")},parentsUntil:function(e,t,n){return Xe(e,"parentNode",n)},next:function(e){return Kt(e,"nextSibling")},prev:function(e){return Kt(e,"previousSibling")},nextAll:function(e){return Xe(e,"nextSibling")},prevAll:function(e){return Xe(e,"previousSibling")},nextUntil:function(e,t,n){return Xe(e,"nextSibling",n)},prevUntil:function(e,t,n){return Xe(e,"previousSibling",n)},siblings:function(e){return Vt((e.parentNode||{}).firstChild,e)},children:function(e){return Vt(e.firstChild)},contents:function(e){return e.contentDocument!=null&&Y(e.contentDocument)?e.contentDocument:(Q(e,"template")&&(e=e.content||e),r.merge([],e.childNodes))}},function(e,t){r.fn[e]=function(n,i){var o=r.map(this,t,n);return e.slice(-5)!=="Until"&&(i=n),i&&typeof i=="string"&&(o=r.filter(i,o)),this.length>1&&(Jn[e]||r.uniqueSort(o),Qn.test(e)&&o.reverse()),this.pushStack(o)}});var he=/[^\x20\t\r\n\f]+/g;function Kn(e){var t={};return r.each(e.match(he)||[],function(n,i){t[i]=!0}),t}r.Callbacks=function(e){e=typeof e=="string"?Kn(e):r.extend({},e);var t,n,i,o,s=[],a=[],l=-1,f=function(){for(o=o||e.once,i=t=!0;a.length;l=-1)for(n=a.shift();++l-1;)s.splice(g,1),g<=l&&l--}),this},has:function(v){return v?r.inArray(v,s)>-1:s.length>0},empty:function(){return s&&(s=[]),this},disable:function(){return o=a=[],s=n="",this},disabled:function(){return!s},lock:function(){return o=a=[],!n&&!t&&(s=n=""),this},locked:function(){return!!o},fireWith:function(v,b){return o||(b=b||[],b=[v,b.slice?b.slice():b],a.push(b),t||f()),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!i}};return d};function _e(e){return e}function ut(e){throw e}function Zt(e,t,n,i){var o;try{e&&M(o=e.promise)?o.call(e).done(t).fail(n):e&&M(o=e.then)?o.call(e,t,n):t.apply(void 0,[e].slice(i))}catch(s){n.apply(void 0,[s])}}r.extend({Deferred:function(e){var t=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],n="pending",i={state:function(){return n},always:function(){return o.done(arguments).fail(arguments),this},catch:function(s){return i.then(null,s)},pipe:function(){var s=arguments;return r.Deferred(function(a){r.each(t,function(l,f){var d=M(s[f[4]])&&s[f[4]];o[f[1]](function(){var v=d&&d.apply(this,arguments);v&&M(v.promise)?v.promise().progress(a.notify).done(a.resolve).fail(a.reject):a[f[0]+"With"](this,d?[v]:arguments)})}),s=null}).promise()},then:function(s,a,l){var f=0;function d(v,b,g,x){return function(){var H=this,B=arguments,R=function(){var K,ye;if(!(v=f&&(g!==ut&&(H=void 0,B=[K]),b.rejectWith(H,B))}};v?Z():(r.Deferred.getErrorHook?Z.error=r.Deferred.getErrorHook():r.Deferred.getStackHook&&(Z.error=r.Deferred.getStackHook()),h.setTimeout(Z))}}return r.Deferred(function(v){t[0][3].add(d(0,v,M(l)?l:_e,v.notifyWith)),t[1][3].add(d(0,v,M(s)?s:_e)),t[2][3].add(d(0,v,M(a)?a:ut))}).promise()},promise:function(s){return s!=null?r.extend(s,i):i}},o={};return r.each(t,function(s,a){var l=a[2],f=a[5];i[a[1]]=l.add,f&&l.add(function(){n=f},t[3-s][2].disable,t[3-s][3].disable,t[0][2].lock,t[0][3].lock),l.add(a[3].fire),o[a[0]]=function(){return o[a[0]+"With"](this===o?void 0:this,arguments),this},o[a[0]+"With"]=l.fireWith}),i.promise(o),e&&e.call(o,o),o},when:function(e){var t=arguments.length,n=t,i=Array(n),o=k.call(arguments),s=r.Deferred(),a=function(l){return function(f){i[l]=this,o[l]=arguments.length>1?k.call(arguments):f,--t||s.resolveWith(i,o)}};if(t<=1&&(Zt(e,s.done(a(n)).resolve,s.reject,!t),s.state()==="pending"||M(o[n]&&o[n].then)))return s.then();for(;n--;)Zt(o[n],a(n),s.reject);return s.promise()}});var Zn=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(e,t){h.console&&h.console.warn&&e&&Zn.test(e.name)&&h.console.warn("jQuery.Deferred exception: "+e.message,e.stack,t)},r.readyException=function(e){h.setTimeout(function(){throw e})};var Et=r.Deferred();r.fn.ready=function(e){return Et.then(e).catch(function(t){r.readyException(t)}),this},r.extend({isReady:!1,readyWait:1,ready:function(e){(e===!0?--r.readyWait:r.isReady)||(r.isReady=!0,!(e!==!0&&--r.readyWait>0)&&Et.resolveWith(P,[r]))}}),r.ready.then=Et.then;function ft(){P.removeEventListener("DOMContentLoaded",ft),h.removeEventListener("load",ft),r.ready()}P.readyState==="complete"||P.readyState!=="loading"&&!P.documentElement.doScroll?h.setTimeout(r.ready):(P.addEventListener("DOMContentLoaded",ft),h.addEventListener("load",ft));var we=function(e,t,n,i,o,s,a){var l=0,f=e.length,d=n==null;if(Ie(n)==="object"){o=!0;for(l in n)we(e,t,l,n[l],!0,s,a)}else if(i!==void 0&&(o=!0,M(i)||(a=!0),d&&(a?(t.call(e,i),t=null):(d=t,t=function(v,b,g){return d.call(r(v),g)})),t))for(;l1,null,!0)},removeData:function(e){return this.each(function(){ie.remove(this,e)})}}),r.extend({queue:function(e,t,n){var i;if(e)return t=(t||"fx")+"queue",i=D.get(e,t),n&&(!i||Array.isArray(n)?i=D.access(e,t,r.makeArray(n)):i.push(n)),i||[]},dequeue:function(e,t){t=t||"fx";var n=r.queue(e,t),i=n.length,o=n.shift(),s=r._queueHooks(e,t),a=function(){r.dequeue(e,t)};o==="inprogress"&&(o=n.shift(),i--),o&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,o.call(e,a,s)),!i&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return D.get(e,n)||D.access(e,n,{empty:r.Callbacks("once memory").add(function(){D.remove(e,[t+"queue",n])})})}}),r.fn.extend({queue:function(e,t){var n=2;return typeof e!="string"&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,sn=/^$|^module$|\/(?:java|ecma)script/i;(function(){var e=P.createDocumentFragment(),t=e.appendChild(P.createElement("div")),n=P.createElement("input");n.setAttribute("type","radio"),n.setAttribute("checked","checked"),n.setAttribute("name","t"),t.appendChild(n),O.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,t.innerHTML="",O.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,t.innerHTML="",O.option=!!t.lastChild})();var ue={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ue.tbody=ue.tfoot=ue.colgroup=ue.caption=ue.thead,ue.th=ue.td,O.option||(ue.optgroup=ue.option=[1,""]);function re(e,t){var n;return typeof e.getElementsByTagName<"u"?n=e.getElementsByTagName(t||"*"):typeof e.querySelectorAll<"u"?n=e.querySelectorAll(t||"*"):n=[],t===void 0||t&&Q(e,t)?r.merge([e],n):n}function Nt(e,t){for(var n=0,i=e.length;n-1){o&&o.push(s);continue}if(d=We(s),a=re(b.appendChild(s),"script"),d&&Nt(a),n)for(v=0;s=a[v++];)sn.test(s.type||"")&&n.push(s)}return b}var un=/^([^.]*)(?:\.(.+)|)/;function Ye(){return!0}function Be(){return!1}function St(e,t,n,i,o,s){var a,l;if(typeof t=="object"){typeof n!="string"&&(i=i||n,n=void 0);for(l in t)St(e,l,n,i,t[l],s);return e}if(i==null&&o==null?(o=n,i=n=void 0):o==null&&(typeof n=="string"?(o=i,i=void 0):(o=i,i=n,n=void 0)),o===!1)o=Be;else if(!o)return e;return s===1&&(a=o,o=function(f){return r().off(f),a.apply(this,arguments)},o.guid=a.guid||(a.guid=r.guid++)),e.each(function(){r.event.add(this,t,o,i,n)})}r.event={global:{},add:function(e,t,n,i,o){var s,a,l,f,d,v,b,g,x,H,B,R=D.get(e);if(Qe(e))for(n.handler&&(s=n,n=s.handler,o=s.selector),o&&r.find.matchesSelector(Le,o),n.guid||(n.guid=r.guid++),(f=R.events)||(f=R.events=Object.create(null)),(a=R.handle)||(a=R.handle=function(Z){return typeof r<"u"&&r.event.triggered!==Z.type?r.event.dispatch.apply(e,arguments):void 0}),t=(t||"").match(he)||[""],d=t.length;d--;)l=un.exec(t[d])||[],x=B=l[1],H=(l[2]||"").split(".").sort(),x&&(b=r.event.special[x]||{},x=(o?b.delegateType:b.bindType)||x,b=r.event.special[x]||{},v=r.extend({type:x,origType:B,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&r.expr.match.needsContext.test(o),namespace:H.join(".")},s),(g=f[x])||(g=f[x]=[],g.delegateCount=0,(!b.setup||b.setup.call(e,i,H,a)===!1)&&e.addEventListener&&e.addEventListener(x,a)),b.add&&(b.add.call(e,v),v.handler.guid||(v.handler.guid=n.guid)),o?g.splice(g.delegateCount++,0,v):g.push(v),r.event.global[x]=!0)},remove:function(e,t,n,i,o){var s,a,l,f,d,v,b,g,x,H,B,R=D.hasData(e)&&D.get(e);if(!(!R||!(f=R.events))){for(t=(t||"").match(he)||[""],d=t.length;d--;){if(l=un.exec(t[d])||[],x=B=l[1],H=(l[2]||"").split(".").sort(),!x){for(x in f)r.event.remove(e,x+t[d],n,i,!0);continue}for(b=r.event.special[x]||{},x=(i?b.delegateType:b.bindType)||x,g=f[x]||[],l=l[2]&&new RegExp("(^|\\.)"+H.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=s=g.length;s--;)v=g[s],(o||B===v.origType)&&(!n||n.guid===v.guid)&&(!l||l.test(v.namespace))&&(!i||i===v.selector||i==="**"&&v.selector)&&(g.splice(s,1),v.selector&&g.delegateCount--,b.remove&&b.remove.call(e,v));a&&!g.length&&((!b.teardown||b.teardown.call(e,H,R.handle)===!1)&&r.removeEvent(e,x,R.handle),delete f[x])}r.isEmptyObject(f)&&D.remove(e,"handle events")}},dispatch:function(e){var t,n,i,o,s,a,l=new Array(arguments.length),f=r.event.fix(e),d=(D.get(this,"events")||Object.create(null))[f.type]||[],v=r.event.special[f.type]||{};for(l[0]=f,t=1;t=1)){for(;d!==this;d=d.parentNode||this)if(d.nodeType===1&&!(e.type==="click"&&d.disabled===!0)){for(s=[],a={},n=0;n-1:r.find(o,this,null,[d]).length),a[o]&&s.push(i);s.length&&l.push({elem:d,handlers:s})}}return d=this,f\s*$/g;function fn(e,t){return Q(e,"table")&&Q(t.nodeType!==11?t:t.firstChild,"tr")&&r(e).children("tbody")[0]||e}function di(e){return e.type=(e.getAttribute("type")!==null)+"/"+e.type,e}function pi(e){return(e.type||"").slice(0,5)==="true/"?e.type=e.type.slice(5):e.removeAttribute("type"),e}function ln(e,t){var n,i,o,s,a,l,f;if(t.nodeType===1){if(D.hasData(e)&&(s=D.get(e),f=s.events,f)){D.remove(t,"handle events");for(o in f)for(n=0,i=f[o].length;n1&&typeof x=="string"&&!O.checkClone&&li.test(x))return e.each(function(B){var R=e.eq(B);H&&(t[0]=x.call(this,B,R.html())),$e(R,t,n,i)});if(b&&(o=an(t,e[0].ownerDocument,!1,e,i),s=o.firstChild,o.childNodes.length===1&&(o=s),s||i)){for(a=r.map(re(o,"script"),di),l=a.length;v0&&Nt(a,!f&&re(e,"script")),l},cleanData:function(e){for(var t,n,i,o=r.event.special,s=0;(n=e[s])!==void 0;s++)if(Qe(n)){if(t=n[D.expando]){if(t.events)for(i in t.events)o[i]?r.event.remove(n,i):r.removeEvent(n,i,t.handle);n[D.expando]=void 0}n[ie.expando]&&(n[ie.expando]=void 0)}}}),r.fn.extend({detach:function(e){return cn(this,e,!0)},remove:function(e){return cn(this,e)},text:function(e){return we(this,function(t){return t===void 0?r.text(this):this.empty().each(function(){(this.nodeType===1||this.nodeType===11||this.nodeType===9)&&(this.textContent=t)})},null,e,arguments.length)},append:function(){return $e(this,arguments,function(e){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var t=fn(this,e);t.appendChild(e)}})},prepend:function(){return $e(this,arguments,function(e){if(this.nodeType===1||this.nodeType===11||this.nodeType===9){var t=fn(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return $e(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return $e(this,arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},empty:function(){for(var e,t=0;(e=this[t])!=null;t++)e.nodeType===1&&(r.cleanData(re(e,!1)),e.textContent="");return this},clone:function(e,t){return e=e??!1,t=t??e,this.map(function(){return r.clone(this,e,t)})},html:function(e){return we(this,function(t){var n=this[0]||{},i=0,o=this.length;if(t===void 0&&n.nodeType===1)return n.innerHTML;if(typeof t=="string"&&!fi.test(t)&&!ue[(on.exec(t)||["",""])[1].toLowerCase()]){t=r.htmlPrefilter(t);try{for(;i=0&&(f+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-s-f-l-.5))||0),f+d}function bn(e,t,n){var i=dt(e),o=!O.boxSizingReliable()||n,s=o&&r.css(e,"boxSizing",!1,i)==="border-box",a=s,l=et(e,t,i),f="offset"+t[0].toUpperCase()+t.slice(1);if(At.test(l)){if(!n)return l;l="auto"}return(!O.boxSizingReliable()&&s||!O.reliableTrDimensions()&&Q(e,"tr")||l==="auto"||!parseFloat(l)&&r.css(e,"display",!1,i)==="inline")&&e.getClientRects().length&&(s=r.css(e,"boxSizing",!1,i)==="border-box",a=f in e,a&&(l=e[f])),l=parseFloat(l)||0,l+jt(e,t,n||(s?"border":"content"),a,i,l)+"px"}r.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=et(e,"opacity");return n===""?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(e,t,n,i){if(!(!e||e.nodeType===3||e.nodeType===8||!e.style)){var o,s,a,l=ge(t),f=Dt.test(t),d=e.style;if(f||(t=kt(l)),a=r.cssHooks[t]||r.cssHooks[l],n!==void 0){if(s=typeof n,s==="string"&&(o=Ke.exec(n))&&o[1]&&(n=nn(e,t,o),s="number"),n==null||n!==n)return;s==="number"&&!f&&(n+=o&&o[3]||(r.cssNumber[l]?"":"px")),!O.clearCloneStyle&&n===""&&t.indexOf("background")===0&&(d[t]="inherit"),(!a||!("set"in a)||(n=a.set(e,n,i))!==void 0)&&(f?d.setProperty(t,n):d[t]=n)}else return a&&"get"in a&&(o=a.get(e,!1,i))!==void 0?o:d[t]}},css:function(e,t,n,i){var o,s,a,l=ge(t),f=Dt.test(t);return f||(t=kt(l)),a=r.cssHooks[t]||r.cssHooks[l],a&&"get"in a&&(o=a.get(e,!0,n)),o===void 0&&(o=et(e,t,i)),o==="normal"&&t in vn&&(o=vn[t]),n===""||n?(s=parseFloat(o),n===!0||isFinite(s)?s||0:o):o}}),r.each(["height","width"],function(e,t){r.cssHooks[t]={get:function(n,i,o){if(i)return vi.test(r.css(n,"display"))&&(!n.getClientRects().length||!n.getBoundingClientRect().width)?dn(n,mi,function(){return bn(n,t,o)}):bn(n,t,o)},set:function(n,i,o){var s,a=dt(n),l=!O.scrollboxSize()&&a.position==="absolute",f=l||o,d=f&&r.css(n,"boxSizing",!1,a)==="border-box",v=o?jt(n,t,o,d,a):0;return d&&l&&(v-=Math.ceil(n["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(a[t])-jt(n,t,"border",!1,a)-.5)),v&&(s=Ke.exec(i))&&(s[3]||"px")!=="px"&&(n.style[t]=i,i=r.css(n,t)),mn(n,i,v)}}}),r.cssHooks.marginLeft=pn(O.reliableMarginLeft,function(e,t){if(t)return(parseFloat(et(e,"marginLeft"))||e.getBoundingClientRect().left-dn(e,{marginLeft:0},function(){return e.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(e,t){r.cssHooks[e+t]={expand:function(n){for(var i=0,o={},s=typeof n=="string"?n.split(" "):[n];i<4;i++)o[e+Ee[i]+t]=s[i]||s[i-2]||s[0];return o}},e!=="margin"&&(r.cssHooks[e+t].set=mn)}),r.fn.extend({css:function(e,t){return we(this,function(n,i,o){var s,a,l={},f=0;if(Array.isArray(i)){for(s=dt(n),a=i.length;f1)}});function oe(e,t,n,i,o){return new oe.prototype.init(e,t,n,i,o)}r.Tween=oe,oe.prototype={constructor:oe,init:function(e,t,n,i,o,s){this.elem=e,this.prop=n,this.easing=o||r.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=i,this.unit=s||(r.cssNumber[n]?"":"px")},cur:function(){var e=oe.propHooks[this.prop];return e&&e.get?e.get(this):oe.propHooks._default.get(this)},run:function(e){var t,n=oe.propHooks[this.prop];return this.options.duration?this.pos=t=r.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):oe.propHooks._default.set(this),this}},oe.prototype.init.prototype=oe.prototype,oe.propHooks={_default:{get:function(e){var t;return e.elem.nodeType!==1||e.elem[e.prop]!=null&&e.elem.style[e.prop]==null?e.elem[e.prop]:(t=r.css(e.elem,e.prop,""),!t||t==="auto"?0:t)},set:function(e){r.fx.step[e.prop]?r.fx.step[e.prop](e):e.elem.nodeType===1&&(r.cssHooks[e.prop]||e.elem.style[kt(e.prop)]!=null)?r.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},oe.propHooks.scrollTop=oe.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},r.easing={linear:function(e){return e},swing:function(e){return .5-Math.cos(e*Math.PI)/2},_default:"swing"},r.fx=oe.prototype.init,r.fx.step={};var ze,pt,bi=/^(?:toggle|show|hide)$/,xi=/queueHooks$/;function Lt(){pt&&(P.hidden===!1&&h.requestAnimationFrame?h.requestAnimationFrame(Lt):h.setTimeout(Lt,r.fx.interval),r.fx.tick())}function xn(){return h.setTimeout(function(){ze=void 0}),ze=Date.now()}function ht(e,t){var n,i=0,o={height:e};for(t=t?1:0;i<4;i+=2-t)n=Ee[i],o["margin"+n]=o["padding"+n]=e;return t&&(o.opacity=o.width=e),o}function Cn(e,t,n){for(var i,o=(de.tweeners[t]||[]).concat(de.tweeners["*"]),s=0,a=o.length;s1)},removeAttr:function(e){return this.each(function(){r.removeAttr(this,e)})}}),r.extend({attr:function(e,t,n){var i,o,s=e.nodeType;if(!(s===3||s===8||s===2)){if(typeof e.getAttribute>"u")return r.prop(e,t,n);if((s!==1||!r.isXMLDoc(e))&&(o=r.attrHooks[t.toLowerCase()]||(r.expr.match.bool.test(t)?Tn:void 0)),n!==void 0){if(n===null){r.removeAttr(e,t);return}return o&&"set"in o&&(i=o.set(e,n,t))!==void 0?i:(e.setAttribute(t,n+""),n)}return o&&"get"in o&&(i=o.get(e,t))!==null?i:(i=r.find.attr(e,t),i??void 0)}},attrHooks:{type:{set:function(e,t){if(!O.radioValue&&t==="radio"&&Q(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,i=0,o=t&&t.match(he);if(o&&e.nodeType===1)for(;n=o[i++];)e.removeAttribute(n)}}),Tn={set:function(e,t,n){return t===!1?r.removeAttr(e,n):e.setAttribute(n,n),n}},r.each(r.expr.match.bool.source.match(/\w+/g),function(e,t){var n=tt[t]||r.find.attr;tt[t]=function(i,o,s){var a,l,f=o.toLowerCase();return s||(l=tt[f],tt[f]=a,a=n(i,o,s)!=null?f:null,tt[f]=l),a}});var wi=/^(?:input|select|textarea|button)$/i,Ei=/^(?:a|area)$/i;r.fn.extend({prop:function(e,t){return we(this,r.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[r.propFix[e]||e]})}}),r.extend({prop:function(e,t,n){var i,o,s=e.nodeType;if(!(s===3||s===8||s===2))return(s!==1||!r.isXMLDoc(e))&&(t=r.propFix[t]||t,o=r.propHooks[t]),n!==void 0?o&&"set"in o&&(i=o.set(e,n,t))!==void 0?i:e[t]=n:o&&"get"in o&&(i=o.get(e,t))!==null?i:e[t]},propHooks:{tabIndex:{get:function(e){var t=r.find.attr(e,"tabindex");return t?parseInt(t,10):wi.test(e.nodeName)||Ei.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),O.optSelected||(r.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function He(e){var t=e.match(he)||[];return t.join(" ")}function qe(e){return e.getAttribute&&e.getAttribute("class")||""}function Ht(e){return Array.isArray(e)?e:typeof e=="string"?e.match(he)||[]:[]}r.fn.extend({addClass:function(e){var t,n,i,o,s,a;return M(e)?this.each(function(l){r(this).addClass(e.call(this,l,qe(this)))}):(t=Ht(e),t.length?this.each(function(){if(i=qe(this),n=this.nodeType===1&&" "+He(i)+" ",n){for(s=0;s-1;)n=n.replace(" "+o+" "," ");a=He(n),i!==a&&this.setAttribute("class",a)}}):this):this.attr("class","")},toggleClass:function(e,t){var n,i,o,s,a=typeof e,l=a==="string"||Array.isArray(e);return M(e)?this.each(function(f){r(this).toggleClass(e.call(this,f,qe(this),t),t)}):typeof t=="boolean"&&l?t?this.addClass(e):this.removeClass(e):(n=Ht(e),this.each(function(){if(l)for(s=r(this),o=0;o-1)return!0;return!1}});var Ni=/\r/g;r.fn.extend({val:function(e){var t,n,i,o=this[0];return arguments.length?(i=M(e),this.each(function(s){var a;this.nodeType===1&&(i?a=e.call(this,s,r(this).val()):a=e,a==null?a="":typeof a=="number"?a+="":Array.isArray(a)&&(a=r.map(a,function(l){return l==null?"":l+""})),t=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],(!t||!("set"in t)||t.set(this,a,"value")===void 0)&&(this.value=a))})):o?(t=r.valHooks[o.type]||r.valHooks[o.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(o,"value"))!==void 0?n:(n=o.value,typeof n=="string"?n.replace(Ni,""):n??"")):void 0}}),r.extend({valHooks:{option:{get:function(e){var t=r.find.attr(e,"value");return t??He(r.text(e))}},select:{get:function(e){var t,n,i,o=e.options,s=e.selectedIndex,a=e.type==="select-one",l=a?null:[],f=a?s+1:o.length;for(s<0?i=f:i=a?s:0;i-1)&&(n=!0);return n||(e.selectedIndex=-1),s}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=r.inArray(r(e).val(),t)>-1}},O.checkOn||(r.valHooks[this].get=function(e){return e.getAttribute("value")===null?"on":e.value})});var nt=h.location,wn={guid:Date.now()},qt=/\?/;r.parseXML=function(e){var t,n;if(!e||typeof e!="string")return null;try{t=new h.DOMParser().parseFromString(e,"text/xml")}catch{}return n=t&&t.getElementsByTagName("parsererror")[0],(!t||n)&&r.error("Invalid XML: "+(n?r.map(n.childNodes,function(i){return i.textContent}).join(`
+`):e)),t};var En=/^(?:focusinfocus|focusoutblur)$/,Nn=function(e){e.stopPropagation()};r.extend(r.event,{trigger:function(e,t,n,i){var o,s,a,l,f,d,v,b,g=[n||P],x=ae.call(e,"type")?e.type:e,H=ae.call(e,"namespace")?e.namespace.split("."):[];if(s=b=a=n=n||P,!(n.nodeType===3||n.nodeType===8)&&!En.test(x+r.event.triggered)&&(x.indexOf(".")>-1&&(H=x.split("."),x=H.shift(),H.sort()),f=x.indexOf(":")<0&&"on"+x,e=e[r.expando]?e:new r.Event(x,typeof e=="object"&&e),e.isTrigger=i?2:3,e.namespace=H.join("."),e.rnamespace=e.namespace?new RegExp("(^|\\.)"+H.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,e.result=void 0,e.target||(e.target=n),t=t==null?[e]:r.makeArray(t,[e]),v=r.event.special[x]||{},!(!i&&v.trigger&&v.trigger.apply(n,t)===!1))){if(!i&&!v.noBubble&&!Re(n)){for(l=v.delegateType||x,En.test(l+x)||(s=s.parentNode);s;s=s.parentNode)g.push(s),a=s;a===(n.ownerDocument||P)&&g.push(a.defaultView||a.parentWindow||h)}for(o=0;(s=g[o++])&&!e.isPropagationStopped();)b=s,e.type=o>1?l:v.bindType||x,d=(D.get(s,"events")||Object.create(null))[e.type]&&D.get(s,"handle"),d&&d.apply(s,t),d=f&&s[f],d&&d.apply&&Qe(s)&&(e.result=d.apply(s,t),e.result===!1&&e.preventDefault());return e.type=x,!i&&!e.isDefaultPrevented()&&(!v._default||v._default.apply(g.pop(),t)===!1)&&Qe(n)&&f&&M(n[x])&&!Re(n)&&(a=n[f],a&&(n[f]=null),r.event.triggered=x,e.isPropagationStopped()&&b.addEventListener(x,Nn),n[x](),e.isPropagationStopped()&&b.removeEventListener(x,Nn),r.event.triggered=void 0,a&&(n[f]=a)),e.result}},simulate:function(e,t,n){var i=r.extend(new r.Event,n,{type:e,isSimulated:!0});r.event.trigger(i,null,t)}}),r.fn.extend({trigger:function(e,t){return this.each(function(){r.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];if(n)return r.event.trigger(e,t,n,!0)}});var Si=/\[\]$/,Sn=/\r?\n/g,Ai=/^(?:submit|button|image|reset|file)$/i,Di=/^(?:input|select|textarea|keygen)/i;function Pt(e,t,n,i){var o;if(Array.isArray(t))r.each(t,function(s,a){n||Si.test(e)?i(e,a):Pt(e+"["+(typeof a=="object"&&a!=null?s:"")+"]",a,n,i)});else if(!n&&Ie(t)==="object")for(o in t)Pt(e+"["+o+"]",t[o],n,i);else i(e,t)}r.param=function(e,t){var n,i=[],o=function(s,a){var l=M(a)?a():a;i[i.length]=encodeURIComponent(s)+"="+encodeURIComponent(l??"")};if(e==null)return"";if(Array.isArray(e)||e.jquery&&!r.isPlainObject(e))r.each(e,function(){o(this.name,this.value)});else for(n in e)Pt(n,e[n],t,o);return i.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=r.prop(this,"elements");return e?r.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!r(this).is(":disabled")&&Di.test(this.nodeName)&&!Ai.test(e)&&(this.checked||!Ze.test(e))}).map(function(e,t){var n=r(this).val();return n==null?null:Array.isArray(n)?r.map(n,function(i){return{name:t.name,value:i.replace(Sn,`\r
+`)}}):{name:t.name,value:n.replace(Sn,`\r
+`)}}).get()}});var ki=/%20/g,ji=/#.*$/,Li=/([?&])_=[^&]*/,Hi=/^(.*?):[ \t]*([^\r\n]*)$/mg,qi=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pi=/^(?:GET|HEAD)$/,Oi=/^\/\//,An={},Ot={},Dn="*/".concat("*"),Mt=P.createElement("a");Mt.href=nt.href;function kn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var i,o=0,s=t.toLowerCase().match(he)||[];if(M(n))for(;i=s[o++];)i[0]==="+"?(i=i.slice(1)||"*",(e[i]=e[i]||[]).unshift(n)):(e[i]=e[i]||[]).push(n)}}function jn(e,t,n,i){var o={},s=e===Ot;function a(l){var f;return o[l]=!0,r.each(e[l]||[],function(d,v){var b=v(t,n,i);if(typeof b=="string"&&!s&&!o[b])return t.dataTypes.unshift(b),a(b),!1;if(s)return!(f=b)}),f}return a(t.dataTypes[0])||!o["*"]&&a("*")}function Rt(e,t){var n,i,o=r.ajaxSettings.flatOptions||{};for(n in t)t[n]!==void 0&&((o[n]?e:i||(i={}))[n]=t[n]);return i&&r.extend(!0,e,i),e}function Mi(e,t,n){for(var i,o,s,a,l=e.contents,f=e.dataTypes;f[0]==="*";)f.shift(),i===void 0&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i){for(o in l)if(l[o]&&l[o].test(i)){f.unshift(o);break}}if(f[0]in n)s=f[0];else{for(o in n){if(!f[0]||e.converters[o+" "+f[0]]){s=o;break}a||(a=o)}s=s||a}if(s)return s!==f[0]&&f.unshift(s),n[s]}function Ri(e,t,n,i){var o,s,a,l,f,d={},v=e.dataTypes.slice();if(v[1])for(a in e.converters)d[a.toLowerCase()]=e.converters[a];for(s=v.shift();s;)if(e.responseFields[s]&&(n[e.responseFields[s]]=t),!f&&i&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),f=s,s=v.shift(),s){if(s==="*")s=f;else if(f!=="*"&&f!==s){if(a=d[f+" "+s]||d["* "+s],!a){for(o in d)if(l=o.split(" "),l[1]===s&&(a=d[f+" "+l[0]]||d["* "+l[0]],a)){a===!0?a=d[o]:d[o]!==!0&&(s=l[0],v.unshift(l[1]));break}}if(a!==!0)if(a&&e.throws)t=a(t);else try{t=a(t)}catch(b){return{state:"parsererror",error:a?b:"No conversion from "+f+" to "+s}}}}return{state:"success",data:t}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:nt.href,type:"GET",isLocal:qi.test(nt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Dn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Rt(Rt(e,r.ajaxSettings),t):Rt(r.ajaxSettings,e)},ajaxPrefilter:kn(An),ajaxTransport:kn(Ot),ajax:function(e,t){typeof e=="object"&&(t=e,e=void 0),t=t||{};var n,i,o,s,a,l,f,d,v,b,g=r.ajaxSetup({},t),x=g.context||g,H=g.context&&(x.nodeType||x.jquery)?r(x):r.event,B=r.Deferred(),R=r.Callbacks("once memory"),Z=g.statusCode||{},K={},ye={},ve="canceled",F={readyState:0,getResponseHeader:function($){var J;if(f){if(!s)for(s={};J=Hi.exec(o);)s[J[1].toLowerCase()+" "]=(s[J[1].toLowerCase()+" "]||[]).concat(J[2]);J=s[$.toLowerCase()+" "]}return J==null?null:J.join(", ")},getAllResponseHeaders:function(){return f?o:null},setRequestHeader:function($,J){return f==null&&($=ye[$.toLowerCase()]=ye[$.toLowerCase()]||$,K[$]=J),this},overrideMimeType:function($){return f==null&&(g.mimeType=$),this},statusCode:function($){var J;if($)if(f)F.always($[F.status]);else for(J in $)Z[J]=[Z[J],$[J]];return this},abort:function($){var J=$||ve;return n&&n.abort(J),Pe(0,J),this}};if(B.promise(F),g.url=((e||g.url||nt.href)+"").replace(Oi,nt.protocol+"//"),g.type=t.method||t.type||g.method||g.type,g.dataTypes=(g.dataType||"*").toLowerCase().match(he)||[""],g.crossDomain==null){l=P.createElement("a");try{l.href=g.url,l.href=l.href,g.crossDomain=Mt.protocol+"//"+Mt.host!=l.protocol+"//"+l.host}catch{g.crossDomain=!0}}if(g.data&&g.processData&&typeof g.data!="string"&&(g.data=r.param(g.data,g.traditional)),jn(An,g,t,F),f)return F;d=r.event&&g.global,d&&r.active++===0&&r.event.trigger("ajaxStart"),g.type=g.type.toUpperCase(),g.hasContent=!Pi.test(g.type),i=g.url.replace(ji,""),g.hasContent?g.data&&g.processData&&(g.contentType||"").indexOf("application/x-www-form-urlencoded")===0&&(g.data=g.data.replace(ki,"+")):(b=g.url.slice(i.length),g.data&&(g.processData||typeof g.data=="string")&&(i+=(qt.test(i)?"&":"?")+g.data,delete g.data),g.cache===!1&&(i=i.replace(Li,"$1"),b=(qt.test(i)?"&":"?")+"_="+wn.guid+++b),g.url=i+b),g.ifModified&&(r.lastModified[i]&&F.setRequestHeader("If-Modified-Since",r.lastModified[i]),r.etag[i]&&F.setRequestHeader("If-None-Match",r.etag[i])),(g.data&&g.hasContent&&g.contentType!==!1||t.contentType)&&F.setRequestHeader("Content-Type",g.contentType),F.setRequestHeader("Accept",g.dataTypes[0]&&g.accepts[g.dataTypes[0]]?g.accepts[g.dataTypes[0]]+(g.dataTypes[0]!=="*"?", "+Dn+"; q=0.01":""):g.accepts["*"]);for(v in g.headers)F.setRequestHeader(v,g.headers[v]);if(g.beforeSend&&(g.beforeSend.call(x,F,g)===!1||f))return F.abort();if(ve="abort",R.add(g.complete),F.done(g.success),F.fail(g.error),n=jn(Ot,g,t,F),!n)Pe(-1,"No Transport");else{if(F.readyState=1,d&&H.trigger("ajaxSend",[F,g]),f)return F;g.async&&g.timeout>0&&(a=h.setTimeout(function(){F.abort("timeout")},g.timeout));try{f=!1,n.send(K,Pe)}catch($){if(f)throw $;Pe(-1,$)}}function Pe($,J,rt,Xt){var me,ot,be,De,ke,fe=J;f||(f=!0,a&&h.clearTimeout(a),n=void 0,o=Xt||"",F.readyState=$>0?4:0,me=$>=200&&$<300||$===304,rt&&(De=Mi(g,F,rt)),!me&&r.inArray("script",g.dataTypes)>-1&&r.inArray("json",g.dataTypes)<0&&(g.converters["text script"]=function(){}),De=Ri(g,De,F,me),me?(g.ifModified&&(ke=F.getResponseHeader("Last-Modified"),ke&&(r.lastModified[i]=ke),ke=F.getResponseHeader("etag"),ke&&(r.etag[i]=ke)),$===204||g.type==="HEAD"?fe="nocontent":$===304?fe="notmodified":(fe=De.state,ot=De.data,be=De.error,me=!be)):(be=fe,($||!fe)&&(fe="error",$<0&&($=0))),F.status=$,F.statusText=(J||fe)+"",me?B.resolveWith(x,[ot,fe,F]):B.rejectWith(x,[F,fe,be]),F.statusCode(Z),Z=void 0,d&&H.trigger(me?"ajaxSuccess":"ajaxError",[F,g,me?ot:be]),R.fireWith(x,[F,fe]),d&&(H.trigger("ajaxComplete",[F,g]),--r.active||r.event.trigger("ajaxStop")))}return F},getJSON:function(e,t,n){return r.get(e,t,n,"json")},getScript:function(e,t){return r.get(e,void 0,t,"script")}}),r.each(["get","post"],function(e,t){r[t]=function(n,i,o,s){return M(i)&&(s=s||o,o=i,i=void 0),r.ajax(r.extend({url:n,type:t,dataType:s,data:i,success:o},r.isPlainObject(n)&&n))}}),r.ajaxPrefilter(function(e){var t;for(t in e.headers)t.toLowerCase()==="content-type"&&(e.contentType=e.headers[t]||"")}),r._evalUrl=function(e,t,n){return r.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(i){r.globalEval(i,t,n)}})},r.fn.extend({wrapAll:function(e){var t;return this[0]&&(M(e)&&(e=e.call(this[0])),t=r(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var n=this;n.firstElementChild;)n=n.firstElementChild;return n}).append(this)),this},wrapInner:function(e){return M(e)?this.each(function(t){r(this).wrapInner(e.call(this,t))}):this.each(function(){var t=r(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=M(e);return this.each(function(n){r(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(e){return this.parent(e).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(e){return!r.expr.pseudos.visible(e)},r.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new h.XMLHttpRequest}catch{}};var Ii={0:200,1223:204},it=r.ajaxSettings.xhr();O.cors=!!it&&"withCredentials"in it,O.ajax=it=!!it,r.ajaxTransport(function(e){var t,n;if(O.cors||it&&!e.crossDomain)return{send:function(i,o){var s,a=e.xhr();if(a.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(s in e.xhrFields)a[s]=e.xhrFields[s];e.mimeType&&a.overrideMimeType&&a.overrideMimeType(e.mimeType),!e.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");for(s in i)a.setRequestHeader(s,i[s]);t=function(l){return function(){t&&(t=n=a.onload=a.onerror=a.onabort=a.ontimeout=a.onreadystatechange=null,l==="abort"?a.abort():l==="error"?typeof a.status!="number"?o(0,"error"):o(a.status,a.statusText):o(Ii[a.status]||a.status,a.statusText,(a.responseType||"text")!=="text"||typeof a.responseText!="string"?{binary:a.response}:{text:a.responseText},a.getAllResponseHeaders()))}},a.onload=t(),n=a.onerror=a.ontimeout=t("error"),a.onabort!==void 0?a.onabort=n:a.onreadystatechange=function(){a.readyState===4&&h.setTimeout(function(){t&&n()})},t=t("abort");try{a.send(e.hasContent&&e.data||null)}catch(l){if(t)throw l}},abort:function(){t&&t()}}}),r.ajaxPrefilter(function(e){e.crossDomain&&(e.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return r.globalEval(e),e}}}),r.ajaxPrefilter("script",function(e){e.cache===void 0&&(e.cache=!1),e.crossDomain&&(e.type="GET")}),r.ajaxTransport("script",function(e){if(e.crossDomain||e.scriptAttrs){var t,n;return{send:function(i,o){t=r("
+@endpush
+
+@push('styles')
+
+@endpush
+
diff --git a/resources/views/components/page-renderer.blade.php b/resources/views/components/page-renderer.blade.php
new file mode 100644
index 0000000..8d431c7
--- /dev/null
+++ b/resources/views/components/page-renderer.blade.php
@@ -0,0 +1,41 @@
+
+ {{-- Render Header Template --}}
+ @if($page->headerTemplate)
+
+ @endif
+
+ {{-- Render Page Sections --}}
+
+ {{-- Legacy sections (if exists) --}}
+ @if($page->sections && count($page->sections) > 0)
+
+ @foreach($page->sections as $section)
+
+ @endforeach
+
+ @endif
+
+ {{-- New template-based sections --}}
+ @if($page->sections_data && count($page->sections_data) > 0)
+
+ {!! $renderSections() !!}
+
+ @endif
+
+ {{-- Fallback to content field if no sections --}}
+ @if((!$page->sections || count($page->sections) === 0) && (!$page->sections_data || count($page->sections_data) === 0))
+
+ {!! $page->content !!}
+
+ @endif
+
+
+ {{-- Render Footer Template --}}
+ @if($page->footerTemplate)
+
+ @endif
+
\ No newline at end of file
diff --git a/resources/views/components/placeholder-picker.blade.php b/resources/views/components/placeholder-picker.blade.php
new file mode 100644
index 0000000..3673209
--- /dev/null
+++ b/resources/views/components/placeholder-picker.blade.php
@@ -0,0 +1,412 @@
+@php
+ // Desteklenen placeholder tipleri ve açıklamaları
+ $placeholderTypes = [
+ 'text' => ['label' => __('placeholder-picker.type_text'), 'examples' => ['company_name', 'title', 'description', 'subtitle']],
+ 'email' => ['label' => __('placeholder-picker.type_email'), 'examples' => ['contact', 'support', 'info', 'sales']],
+ 'url' => ['label' => __('placeholder-picker.type_url'), 'examples' => ['website', 'facebook', 'twitter', 'linkedin']],
+ 'tel' => ['label' => __('placeholder-picker.type_tel'), 'examples' => ['phone', 'mobile', 'fax', 'hotline']],
+ 'number' => ['label' => __('placeholder-picker.type_number'), 'examples' => ['price', 'quantity', 'age', 'year']],
+ 'textarea' => ['label' => __('placeholder-picker.type_textarea'), 'examples' => ['content', 'about', 'description', 'notes']],
+ 'richtext' => ['label' => __('placeholder-picker.type_richtext'), 'examples' => ['content', 'article', 'post', 'body']],
+ 'markdown' => ['label' => __('placeholder-picker.type_markdown'), 'examples' => ['content', 'documentation', 'readme']],
+ 'code' => ['label' => __('placeholder-picker.type_code'), 'examples' => ['snippet', 'example', 'script']],
+ 'date' => ['label' => __('placeholder-picker.type_date'), 'examples' => ['publish_date', 'event_date', 'birthday']],
+ 'datetime' => ['label' => __('placeholder-picker.type_datetime'), 'examples' => ['created_at', 'updated_at', 'event_time']],
+ 'time' => ['label' => __('placeholder-picker.type_time'), 'examples' => ['start_time', 'end_time', 'opening_time']],
+ 'image' => ['label' => __('placeholder-picker.type_image'), 'examples' => ['logo', 'banner', 'avatar', 'thumbnail']],
+ 'images' => ['label' => __('placeholder-picker.type_images'), 'examples' => ['gallery', 'photos', 'slider_images']],
+ 'file' => ['label' => __('placeholder-picker.type_file'), 'examples' => ['document', 'pdf', 'download']],
+ 'files' => ['label' => __('placeholder-picker.type_files'), 'examples' => ['documents', 'attachments']],
+ 'select' => ['label' => __('placeholder-picker.type_select'), 'examples' => ['category', 'status', 'type']],
+ 'multiselect' => ['label' => __('placeholder-picker.type_multiselect'), 'examples' => ['tags', 'categories', 'features']],
+ 'checkbox' => ['label' => __('placeholder-picker.type_checkbox'), 'examples' => ['is_active', 'is_featured', 'is_published']],
+ 'radio' => ['label' => __('placeholder-picker.type_radio'), 'examples' => ['gender', 'type', 'status']],
+ 'toggle' => ['label' => __('placeholder-picker.type_toggle'), 'examples' => ['is_active', 'is_enabled', 'is_visible']],
+ 'color' => ['label' => __('placeholder-picker.type_color'), 'examples' => ['primary_color', 'background_color', 'text_color']],
+ 'tags' => ['label' => __('placeholder-picker.type_tags'), 'examples' => ['tags', 'keywords', 'labels']],
+ ];
+
+ // Custom klasöründeki blade dosyalarını tespit et ve placeholder types'a ekle
+ $customPlaceholders = [];
+ $customPath = resource_path('views/components/custom');
+
+ if (is_dir($customPath)) {
+ $files = scandir($customPath);
+
+ foreach ($files as $file) {
+ // . ve .. dizinlerini atla
+ if ($file === '.' || $file === '..') {
+ continue;
+ }
+
+ // Sadece .blade.php uzantılı dosyaları al
+ $filePath = $customPath . DIRECTORY_SEPARATOR . $file;
+ if (is_file($filePath) && str_ends_with($file, '.blade.php')) {
+ $name = str_replace('.blade.php', '', $file);
+ $customPlaceholders[] = $name;
+ }
+ }
+ sort($customPlaceholders);
+
+ // Custom placeholder'ları placeholder types'a ekle
+ if (!empty($customPlaceholders)) {
+ $placeholderTypes['custom'] = [
+ 'label' => __('placeholder-picker.type_custom'),
+ 'examples' => $customPlaceholders
+ ];
+ }
+ }
+
+ // Field name'i component'ten al
+ $fieldName = $fieldName ?? 'html_content';
+@endphp
+
+
+
+
+ {{ __('placeholder-picker.title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ __('placeholder-picker.no_results') }}
+
+
+
+
+
+
+
+
diff --git a/resources/views/components/static-menu.blade.php b/resources/views/components/static-menu.blade.php
new file mode 100644
index 0000000..b7423ce
--- /dev/null
+++ b/resources/views/components/static-menu.blade.php
@@ -0,0 +1,45 @@
+ function ($query) {
+ $query->where('status', 'published')
+ ->where('show_in_menu', true)
+ ->orderBy('sort_order', 'asc')
+ ->orderBy('title', 'asc');
+ }])
+ ->whereNull('parent_id')
+ ->where('status', 'published')
+ ->where('show_in_menu', true)
+ ->orderBy('sort_order', 'asc')
+ ->orderBy('title', 'asc')
+ ->get();
+ ?>
+
+@if($menuItems->isNotEmpty())
+
+@endif
+
diff --git a/resources/views/components/template-preview.blade.php b/resources/views/components/template-preview.blade.php
new file mode 100644
index 0000000..694e3a5
--- /dev/null
+++ b/resources/views/components/template-preview.blade.php
@@ -0,0 +1,194 @@
+@php
+ $previewUrl = route('template.preview');
+ $type = $type ?? 'section';
+ $fieldName = $fieldName ?? 'html_content';
+ $recordId = $recordId ?? null;
+@endphp
+
+
+
+
+
+
+
+
+
+
+
+
+
+@push('scripts')
+
+@endpush
+
+@push('styles')
+
+@endpush
+
diff --git a/resources/views/filament/admin/pages/user-guide.blade.php b/resources/views/filament/admin/pages/user-guide.blade.php
new file mode 100644
index 0000000..9f204bd
--- /dev/null
+++ b/resources/views/filament/admin/pages/user-guide.blade.php
@@ -0,0 +1,211 @@
+
+
+ {{-- Sol Navigation Bar --}}
+