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 +
+ {text.site_name} + + {email.contact} +
+``` + +### 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 +
+ My Company + + info@company.com +
+``` + +## 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 --}} + + + {{-- Sağ İçerik Alanı --}} +
+ @if($selectedGuide && $this->getSelectedGuideContent()) + @php + $guide = $this->getSelectedGuideContent(); + @endphp + + + +
+

+ {{ \Illuminate\Support\Str::title(str_replace(['-', '_'], ' ', $guide['name'])) }} +

+ @if(isset($guide['modified_date'])) +

+ + {{ __('user-guide.last_updated') }}: + {{ \Carbon\Carbon::parse($guide['modified_date'])->locale(app()->getLocale())->format('d F Y, H:i') }} +

+ @endif +
+
+ + +
+ {!! \Illuminate\Support\Str::markdown($guide['content'], [ + 'html_input' => 'allow', + 'allow_unsafe_links' => false, + ]) !!} +
+
+
+ @else + + + {{ __('user-guide.title') }} + + + +
+ +

+ {{ __('user-guide.no_guide_selected') }} +

+

+ {{ __('user-guide.select_guide_from_menu') }} +

+
+
+
+ @endif +
+
+ + @push('styles') + + @endpush +
diff --git a/resources/views/filament/pages/edit-page-footer.blade.php b/resources/views/filament/pages/edit-page-footer.blade.php new file mode 100644 index 0000000..41f4333 --- /dev/null +++ b/resources/views/filament/pages/edit-page-footer.blade.php @@ -0,0 +1,61 @@ +{{-- Sticky Header CSS --}} + + +@if($autoSaveEnabled) +{{-- Otomatik Kaydetme Script --}} + +@endif + diff --git a/resources/views/filament/resources/pages/edit-with-sticky-actions.blade.php b/resources/views/filament/resources/pages/edit-with-sticky-actions.blade.php new file mode 100644 index 0000000..f4e6569 --- /dev/null +++ b/resources/views/filament/resources/pages/edit-with-sticky-actions.blade.php @@ -0,0 +1,41 @@ +@php + $hasInlineLabels = $this->hasInlineLabels(); +@endphp + + + + +
+ {{ $this->form }} +
+
+ diff --git a/WEBSITE_SETUP.md b/resources/views/guide/WEBSITE_SETUP.md similarity index 100% rename from WEBSITE_SETUP.md rename to resources/views/guide/WEBSITE_SETUP.md diff --git a/resources/views/guide/getting-started.md b/resources/views/guide/getting-started.md new file mode 100644 index 0000000..57d76a7 --- /dev/null +++ b/resources/views/guide/getting-started.md @@ -0,0 +1,37 @@ +# Başlangıç Kılavuzu + +Bu kılavuz, Citrus Platform'a başlangıç yapmak için gerekli adımları içerir. + +## Giriş + +Citrus Platform, modüler yapısı ve güçlü özellikleri ile modern web uygulamaları geliştirmenizi sağlar. + +## Kurulum + +### Gereksinimler + +- PHP 8.1 veya üzeri +- Composer +- MySQL/MariaDB +- Node.js ve NPM + +### Adımlar + +1. Projeyi klonlayın +2. Bağımlılıkları yükleyin: `composer install` +3. Ortam değişkenlerini ayarlayın: `.env` dosyasını düzenleyin +4. Veritabanını oluşturun: `php artisan migrate` +5. Uygulamayı başlatın: `php artisan serve` + +## İlk Adımlar + +Sisteme giriş yaptıktan sonra: + +- Dashboard'u keşfedin +- Kullanıcı yönetimini inceleyin +- Sayfa oluşturma özelliklerini test edin + +## Yardım + +Sorularınız için lütfen dokümantasyonu inceleyin veya destek ekibi ile iletişime geçin. + diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php deleted file mode 100644 index 4cf4c76..0000000 --- a/resources/views/home.blade.php +++ /dev/null @@ -1,155 +0,0 @@ -@extends('layouts.app') - -@section('title', $page ? ($page->meta_title ?? $page->title) : 'Trunçgil Teknoloji') -@section('description', $page ? ($page->meta_description ?? $page->excerpt) : 'Trunçgil Teknoloji - Yenilikçi Çözümler') - -@section('content') - - -
-
- -
-
-
- -
-
- -
- @if($page) -

- {{ $page->title }} -

- @if($page->excerpt) -

- {{ $page->excerpt }} -

- @endif - @else -

- Hoş Geldiniz - - Trunçgil Teknoloji - -

-

- Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz -

- @endif - - -
- - -
-
-
- Trunçgil Teknoloji - -
-
-
-
-
- - @if($page && $page->content) - -
-
-
- {!! $page->content !!} -
-
-
- @endif - - -
-
-
-

- Neden Trunçgil Teknoloji? -

-

- Modern teknolojiler ve uzman ekibimizle projelerinizi hayata geçiriyoruz -

-
- -
- -
-
- - - -
-

- Hızlı Çözümler -

-

- Modern teknolojiler kullanarak projelerinizi hızlı ve verimli bir şekilde hayata geçiriyoruz. -

-
- - -
-
- - - -
-

- Güvenli Altyapı -

-

- En son güvenlik standartlarını kullanarak verilerinizi koruma altına alıyoruz. -

-
- - -
-
- - - -
-

- Uzman Ekip -

-

- Deneyimli ve uzman ekibimiz ile her zaman yanınızdayız. -

-
-
-
-
- - -
-
-

- Projenizi Konuşalım -

-

- Size özel çözümler geliştirmek için iletişime geçin -

- - İletişime Geç - - - - -
-
-@endsection diff --git a/resources/views/layouts/site.blade.php b/resources/views/layouts/site.blade.php new file mode 100644 index 0000000..8c8b57e --- /dev/null +++ b/resources/views/layouts/site.blade.php @@ -0,0 +1,41 @@ + + + + + + {{ $meta['title'] ?? ($settings->default_meta_title ?? config('app.name')) }} + + + + + + + + + + + + + {{-- Dynamic Header Template or Static Navbar --}} + @if(isset($renderedHeader) && $renderedHeader) + {!! $renderedHeader !!} + @else + @include('partials.navbar') + @endif + + {{-- Main Content --}} + @yield('content') + + {{-- Dynamic Footer Template or Static Footer --}} + @if(isset($renderedFooter) && $renderedFooter) + {!! $renderedFooter !!} + @else + @include('partials.footer') + @endif + + + + + + + diff --git a/resources/views/page.blade.php b/resources/views/page.blade.php deleted file mode 100644 index b67e128..0000000 --- a/resources/views/page.blade.php +++ /dev/null @@ -1,103 +0,0 @@ -@extends('layouts.app') - -@section('title', $page->meta_title ?? $page->title) -@section('description', $page->meta_description ?? $page->excerpt ?? '') - -@section('content') - - -
-
-
-
-
- -
-
-

- {{ $page->title }} -

- - @if($page->excerpt) -

- {{ $page->excerpt }} -

- @endif - - @if($page->published_at) -
- - - - {{ $page->published_at->format('d M Y') }} -
- @endif -
-
-
- - -
-
- @if($page->featured_image) -
- {{ $page->title }} -
- @endif - -
- {!! $page->content !!} -
- - - @if($page->children->where('status', 'published')->count() > 0) -
-

- İlgili Sayfalar -

-
- @foreach($page->children->where('status', 'published')->sortBy('sort_order') as $child) - -

- {{ $child->title }} -

- @if($child->excerpt) -

- {{ $child->excerpt }} -

- @endif -
- @endforeach -
-
- @endif - - -
- - - - - Ana Sayfaya Dön - - - @if($page->parent) - - Üst Sayfa - - - - - @endif -
-
-
-@endsection diff --git a/resources/views/partials/footer.blade.php b/resources/views/partials/footer.blade.php new file mode 100644 index 0000000..1c6db7f --- /dev/null +++ b/resources/views/partials/footer.blade.php @@ -0,0 +1,14 @@ +
+
+

{{ ($settings->site_name ?? config('app.name')) }} © {{ now()->year }}

+ +
+
+ + diff --git a/resources/views/partials/navbar.blade.php b/resources/views/partials/navbar.blade.php new file mode 100644 index 0000000..fad1f65 --- /dev/null +++ b/resources/views/partials/navbar.blade.php @@ -0,0 +1,537 @@ +
+ + +
\ No newline at end of file diff --git a/resources/views/templates/generic.blade.php b/resources/views/templates/generic.blade.php new file mode 100644 index 0000000..7e2b762 --- /dev/null +++ b/resources/views/templates/generic.blade.php @@ -0,0 +1,40 @@ +@extends('layouts.site') + +@section('content') + @php + // Yeni dinamik template sistemi - Öncelik verilir + $hasTemplatedSections = isset($templatedSections) && $templatedSections->isNotEmpty(); + + // Eski section builder sistemi + $blocks = $sections ?? []; + $hasBlocks = is_array($blocks) && !empty($blocks); + @endphp + + @if($hasTemplatedSections) + {{-- Yeni dinamik template sisteminden gelen sections --}} + @foreach($templatedSections as $section) + @if($section['template'] ?? null) + {!! \App\Services\TemplateService::replacePlaceholders( + $section['template']->html_content, + $section['data'] ?? [] + ) !!} + @endif + @endforeach + @elseif($hasBlocks) + {{-- Eski blok bazlı dinamik render --}} + @foreach($blocks as $block) + @php $type = $block['type'] ?? null; @endphp + @if($type && view()->exists("components.blocks.$type")) + + @endif + @endforeach + @else + {{-- Hiç blok yoksa placeholder göster --}} +
+

Bu sayfa henüz içerik eklenmemiş

+

Admin panelinden bu sayfaya bloklar ekleyebilirsiniz.

+
+ @endif +@endsection + + diff --git a/resources/views/templates/home.blade.php b/resources/views/templates/home.blade.php new file mode 100644 index 0000000..a88e1d7 --- /dev/null +++ b/resources/views/templates/home.blade.php @@ -0,0 +1,52 @@ +@extends('layouts.site') + +@section('content') + @php + // Yeni dinamik template sistemi - Öncelik verilir + $hasTemplatedSections = isset($templatedSections) && $templatedSections->isNotEmpty(); + + // Eski section builder sistemi + $blocks = $sections ?? []; + $hasBlocks = is_array($blocks) && count($blocks) > 0; + @endphp + + @if($hasTemplatedSections) + {{-- Yeni dinamik template sisteminden gelen sections --}} + @foreach($templatedSections as $section) + @if($section['template'] ?? null) + {!! \App\Services\TemplateService::replacePlaceholders( + $section['template']->html_content, + $section['data'] ?? [] + ) !!} + @endif + @endforeach + @elseif($hasBlocks) + {{-- Eski blok bazlı dinamik render --}} + @foreach($blocks as $block) + @php $type = $block['type'] ?? null; @endphp + @if($type && view()->exists("components.blocks.$type")) + + @endif + @endforeach + @else + {{-- Statik fallback: public/html/index.html içeriğinin bölümü --}} + @php + $indexPath = public_path('html/index.html'); + $bodyHtml = ''; + if (file_exists($indexPath) && is_readable($indexPath)) { + $html = file_get_contents($indexPath); + // Body içeriğini çıkar + if (preg_match('/]*>(.*?)<\/body>/is', $html, $m)) { + $bodyHtml = $m[1]; + } + } + // Eğer body boşsa placeholder göster + if (empty(trim($bodyHtml))) { + $bodyHtml = '

Admin panelinden anasayfa içeriği ekleyin veya index.html kontrol edin.

'; + } + @endphp + {!! $bodyHtml !!} + @endif +@endsection + + diff --git a/routes/web.php b/routes/web.php index 384e847..69f1e62 100644 --- a/routes/web.php +++ b/routes/web.php @@ -2,6 +2,83 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\PageController; +use App\Http\Controllers\BlogController; +use App\Http\Controllers\TemplatePreviewController; +use App\Models\Page; -Route::get('/', [PageController::class, 'index'])->name('home'); +// Debug Route - Veritabanı kontrolü +Route::get('/debug-homepage', function () { + $page = Page::with(['headerTemplate', 'footerTemplate']) + ->where('is_homepage', true) + ->where('status', 'published') + ->latest('updated_at') + ->first(); + + if (!$page) { + return response()->json([ + 'error' => 'Homepage bulunamadı!', + 'all_pages' => Page::select('id', 'title', 'slug', 'is_homepage', 'status')->get(), + ]); + } + + return response()->json([ + 'page' => [ + 'id' => $page->id, + 'title' => $page->title, + 'slug' => $page->slug, + 'is_homepage' => $page->is_homepage, + 'status' => $page->status, + ], + 'header_template' => [ + 'id' => $page->header_template_id, + 'exists' => $page->headerTemplate ? true : false, + 'title' => $page->headerTemplate?->title, + 'data_filled' => !empty($page->header_data), + 'data' => $page->header_data, + ], + 'footer_template' => [ + 'id' => $page->footer_template_id, + 'exists' => $page->footerTemplate ? true : false, + 'title' => $page->footerTemplate?->title, + 'data_filled' => !empty($page->footer_data), + 'data' => $page->footer_data, + ], + 'sections' => [ + 'count' => is_array($page->sections) ? count($page->sections) : 0, + 'data' => $page->sections, + ], + 'sections_data' => [ + 'count' => is_array($page->sections_data) ? count($page->sections_data) : 0, + 'data' => $page->sections_data, + ], + ]); +}); + +// Language Switch +Route::get('/lang/{locale}', function ($locale) { + if (function_exists('switch_language')) { + switch_language($locale); + } else { + $language = \App\Models\Language::findByCode($locale); + if ($language && $language->is_active) { + app()->setLocale($locale); + session(['locale' => $locale]); + } + } + return redirect()->back(); +})->name('language.switch'); + +// Homepage -> admin panelinde "is_homepage" olarak işaretli sayfa dinamik olarak gösterilir +Route::get('/', [PageController::class, 'index'])->name('homepage'); + +// Blog +Route::get('/blog', [BlogController::class, 'index'])->name('blog.index'); +Route::get('/blog/{slug}', [BlogController::class, 'show'])->name('blog.show'); + +// Template Preview (admin panel için) +Route::post('/admin/template-preview', [TemplatePreviewController::class, 'preview']) + ->middleware(['auth']) + ->name('template.preview'); + +// Pages (en sonda olmalı - catch-all) Route::get('/{slug}', [PageController::class, 'show'])->name('page.show');