Merge pull request #12 from truncgil/feature/dynamic-template-system
Feature/dynamic template system
This commit is contained in:
+168
-1
@@ -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ı
|
||||
<?php
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Pages;
|
||||
|
||||
use App\Services\GuideService;
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
|
||||
class UserGuide extends Page
|
||||
{
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedBookOpen;
|
||||
|
||||
protected string $view = 'filament.admin.pages.user-guide';
|
||||
|
||||
protected static ?int $navigationSort = 999;
|
||||
|
||||
public ?string $selectedGuide = null;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$guideService = app(GuideService::class);
|
||||
$firstGuide = $guideService->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates;
|
||||
|
||||
use App\Filament\Admin\Resources\FooterTemplates\Pages\CreateFooterTemplate;
|
||||
use App\Filament\Admin\Resources\FooterTemplates\Pages\EditFooterTemplate;
|
||||
use App\Filament\Admin\Resources\FooterTemplates\Pages\ListFooterTemplates;
|
||||
use App\Filament\Admin\Resources\FooterTemplates\Pages\ViewFooterTemplate;
|
||||
use App\Filament\Admin\Resources\FooterTemplates\Schemas\FooterTemplateForm;
|
||||
use App\Filament\Admin\Resources\FooterTemplates\Schemas\FooterTemplateInfolist;
|
||||
use App\Filament\Admin\Resources\FooterTemplates\Tables\FooterTemplatesTable;
|
||||
use App\Models\FooterTemplate;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class FooterTemplateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = FooterTemplate::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return __('footer-templates.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('footer-templates.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('footer-templates.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('footer-templates.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return FooterTemplateForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return FooterTemplateInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return FooterTemplatesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\FooterTemplates\FooterTemplateResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class CreateFooterTemplate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = FooterTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Transform nested default_data fields (e.g., default_data.color.bg)
|
||||
// into proper array format
|
||||
$defaultData = [];
|
||||
|
||||
foreach ($data as $key => $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\FooterTemplates\FooterTemplateResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class EditFooterTemplate extends EditRecord
|
||||
{
|
||||
protected static string $resource = FooterTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Transform nested default_data fields (e.g., default_data.color.bg)
|
||||
// into proper array format
|
||||
$defaultData = [];
|
||||
|
||||
foreach ($data as $key => $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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\FooterTemplates\FooterTemplateResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListFooterTemplates extends ListRecords
|
||||
{
|
||||
protected static string $resource = FooterTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\FooterTemplates\FooterTemplateResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewFooterTemplate extends ViewRecord
|
||||
{
|
||||
protected static string $resource = FooterTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates\Schemas;
|
||||
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Filament\Forms\Components\CodeEditor;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\CodeEditor\Enums\Language;
|
||||
|
||||
|
||||
class FooterTemplateForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class FooterTemplateInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
//
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\FooterTemplates\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class FooterTemplatesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates;
|
||||
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\Pages\CreateHeaderTemplate;
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\Pages\EditHeaderTemplate;
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\Pages\ListHeaderTemplates;
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\Pages\ViewHeaderTemplate;
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\Schemas\HeaderTemplateForm;
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\Schemas\HeaderTemplateInfolist;
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\Tables\HeaderTemplatesTable;
|
||||
use App\Models\HeaderTemplate;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class HeaderTemplateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = HeaderTemplate::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return __('header-templates.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('header-templates.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('header-templates.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('header-templates.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return HeaderTemplateForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return HeaderTemplateInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return HeaderTemplatesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\HeaderTemplateResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class CreateHeaderTemplate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = HeaderTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Transform nested default_data fields (e.g., default_data.color.bg)
|
||||
// into proper array format
|
||||
$defaultData = [];
|
||||
|
||||
foreach ($data as $key => $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\HeaderTemplateResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class EditHeaderTemplate extends EditRecord
|
||||
{
|
||||
protected static string $resource = HeaderTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Transform nested default_data fields (e.g., default_data.color.bg)
|
||||
// into proper array format
|
||||
$defaultData = [];
|
||||
|
||||
foreach ($data as $key => $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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\HeaderTemplateResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListHeaderTemplates extends ListRecords
|
||||
{
|
||||
protected static string $resource = HeaderTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\HeaderTemplates\HeaderTemplateResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewHeaderTemplate extends ViewRecord
|
||||
{
|
||||
protected static string $resource = HeaderTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates\Schemas;
|
||||
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Filament\Forms\Components\CodeEditor;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\CodeEditor\Enums\Language;
|
||||
|
||||
class HeaderTemplateForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class HeaderTemplateInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
//
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\HeaderTemplates\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class HeaderTemplatesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\CreateMenuTemplate;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\EditMenuTemplate;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\ListMenuTemplates;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Pages\ViewMenuTemplate;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Schemas\MenuTemplateForm;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Schemas\MenuTemplateInfolist;
|
||||
use App\Filament\Admin\Resources\MenuTemplates\Tables\MenuTemplatesTable;
|
||||
use App\Models\MenuTemplate;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class MenuTemplateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = MenuTemplate::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return __('menu-templates.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('menu-templates.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('menu-templates.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('menu-templates.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return MenuTemplateForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return MenuTemplateInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return MenuTemplatesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class CreateMenuTemplate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class EditMenuTemplate extends EditRecord
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
// Kaydetme sonrası preview'ı güncelle
|
||||
// Livewire event dispatch et
|
||||
$this->dispatch('template-saved');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListMenuTemplates extends ListRecords
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\MenuTemplates\MenuTemplateResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewMenuTemplate extends ViewRecord
|
||||
{
|
||||
protected static string $resource = MenuTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Schemas;
|
||||
|
||||
use App\Models\MenuTemplate;
|
||||
use Filament\Forms\Components\CodeEditor;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\CodeEditor\Enums\Language;
|
||||
|
||||
class MenuTemplateForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class MenuTemplateInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
//
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\MenuTemplates\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class MenuTemplatesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\Pages\PageResource;
|
||||
use App\Filament\Admin\Widgets\PagesMenuWidget;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Resources\Pages\Page;
|
||||
|
||||
class MenuTree extends Page
|
||||
{
|
||||
protected static string $resource = PageResource::class;
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return __('pages.menu_tree_title');
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Action::make('refresh')
|
||||
->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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,209 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\ContentSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\FooterTemplateSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\HeaderTemplateSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\PageSettingsSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionBuilderSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionTemplatesSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection;
|
||||
use App\Filament\Admin\Resources\Pages\Schemas\Sections\TranslationsSection;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class PageForm
|
||||
{
|
||||
/**
|
||||
* Configure the page form schema with modular sections.
|
||||
*
|
||||
* This form is organized into 8 sections for better maintainability:
|
||||
* 1. Content - Basic page content (title, slug, content, excerpt)
|
||||
* 2. Page Settings - Metadata and settings (image, author, status, etc.)
|
||||
* 3. SEO - SEO metadata (meta title, meta description)
|
||||
* 4. Section Builder - Dynamic page sections
|
||||
* 5. Header Template - Dynamic header template system
|
||||
* 6. Section Templates - Repeatable section templates
|
||||
* 7. Footer Template - Dynamic footer template system
|
||||
* 8. Translations - Multi-language support
|
||||
*/
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
|
||||
class ContentSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
class FooterTemplateSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('pages.form_section_footer_template'))
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
class HeaderTemplateSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('pages.form_section_header_template'))
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
|
||||
class PageSettingsSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\CodeEditor;
|
||||
use Filament\Forms\Components\CodeEditor\Enums\Language;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\DatePicker;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\MarkdownEditor;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\RichEditor;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class SectionBuilderSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('pages.form_section_sections'))
|
||||
->schema([
|
||||
Repeater::make('sections')
|
||||
->label(__('pages.sections_field'))
|
||||
->helperText(__('pages.sections_helper_text'))
|
||||
->schema([
|
||||
Select::make('type')
|
||||
->label(__('pages.section_type'))
|
||||
->helperText(__('pages.section_type_helper'))
|
||||
->required()
|
||||
->options([
|
||||
'hero' => __('pages.section_type_hero'),
|
||||
'features' => __('pages.section_type_features'),
|
||||
'stats' => __('pages.section_type_stats'),
|
||||
'cta' => __('pages.section_type_cta'),
|
||||
'content' => __('pages.section_type_content'),
|
||||
'gallery' => __('pages.section_type_gallery'),
|
||||
'testimonials' => __('pages.section_type_testimonials'),
|
||||
'team' => __('pages.section_type_team'),
|
||||
'pricing' => __('pages.section_type_pricing'),
|
||||
'faq' => __('pages.section_type_faq'),
|
||||
'contact' => __('pages.section_type_contact'),
|
||||
'custom' => __('pages.section_type_custom'),
|
||||
])
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
Repeater::make('data')
|
||||
->label(__('pages.section_data'))
|
||||
->helperText(__('pages.section_data_helper'))
|
||||
->schema([
|
||||
TextInput::make('key')
|
||||
->label(__('pages.section_data_key'))
|
||||
->helperText(__('pages.section_data_key_helper'))
|
||||
->required()
|
||||
->placeholder('title, subtitle, image, button_text, ...')
|
||||
->columnSpan(1),
|
||||
|
||||
Select::make('type')
|
||||
->label(__('pages.section_data_value_type'))
|
||||
->required()
|
||||
->options([
|
||||
'text' => __('pages.value_type_text'),
|
||||
'textarea' => __('pages.value_type_textarea'),
|
||||
'html' => __('pages.value_type_html'),
|
||||
'markdown' => __('pages.value_type_markdown'),
|
||||
'richtext' => __('pages.value_type_richtext'),
|
||||
'image' => __('pages.value_type_image'),
|
||||
'file' => __('pages.value_type_file'),
|
||||
'url' => __('pages.value_type_url'),
|
||||
'email' => __('pages.value_type_email'),
|
||||
'phone' => __('pages.value_type_phone'),
|
||||
'number' => __('pages.value_type_number'),
|
||||
'boolean' => __('pages.value_type_boolean'),
|
||||
'color' => __('pages.value_type_color'),
|
||||
'date' => __('pages.value_type_date'),
|
||||
'datetime' => __('pages.value_type_datetime'),
|
||||
'array' => __('pages.value_type_array'),
|
||||
'json' => __('pages.value_type_json'),
|
||||
])
|
||||
->live()
|
||||
->columnSpan(1),
|
||||
|
||||
// Single Hidden Field to store the actual value
|
||||
Hidden::make('value'),
|
||||
|
||||
// Text Input
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use App\Models\SectionTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
|
||||
class SectionTemplatesSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return Section::make(__('pages.form_section_template_sections'))
|
||||
->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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
|
||||
class SeoSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use App\Filament\Admin\Resources\Components\TranslationTabs;
|
||||
use Filament\Schemas\Components\Section;
|
||||
|
||||
class TranslationsSection
|
||||
{
|
||||
public static function make(): Section
|
||||
{
|
||||
return 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SectionTemplates\SectionTemplateResource;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class CreateSectionTemplate extends CreateRecord
|
||||
{
|
||||
protected static string $resource = SectionTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Transform nested default_data fields (e.g., default_data.color.bg)
|
||||
// into proper array format
|
||||
$defaultData = [];
|
||||
|
||||
foreach ($data as $key => $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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SectionTemplates\SectionTemplateResource;
|
||||
use Filament\Actions\DeleteAction;
|
||||
use Filament\Actions\ForceDeleteAction;
|
||||
use Filament\Actions\RestoreAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Filament\Support\Enums\Width;
|
||||
|
||||
class EditSectionTemplate extends EditRecord
|
||||
{
|
||||
protected static string $resource = SectionTemplateResource::class;
|
||||
|
||||
public function getMaxContentWidth(): Width | string | null
|
||||
{
|
||||
return Width::Full;
|
||||
}
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
ViewAction::make(),
|
||||
DeleteAction::make(),
|
||||
ForceDeleteAction::make(),
|
||||
RestoreAction::make(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
// Transform nested default_data fields (e.g., default_data.color.bg)
|
||||
// into proper array format
|
||||
$defaultData = [];
|
||||
|
||||
foreach ($data as $key => $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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SectionTemplates\SectionTemplateResource;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListSectionTemplates extends ListRecords
|
||||
{
|
||||
protected static string $resource = SectionTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates\Pages;
|
||||
|
||||
use App\Filament\Admin\Resources\SectionTemplates\SectionTemplateResource;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewSectionTemplate extends ViewRecord
|
||||
{
|
||||
protected static string $resource = SectionTemplateResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
EditAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates\Schemas;
|
||||
|
||||
use App\Models\SectionTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use Filament\Forms\Components\CodeEditor;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Schemas\Components\Group;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\View;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Forms\Components\CodeEditor\Enums\Language;
|
||||
|
||||
class SectionTemplateForm
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates\Schemas;
|
||||
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class SectionTemplateInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
//
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates;
|
||||
|
||||
use App\Filament\Admin\Resources\SectionTemplates\Pages\CreateSectionTemplate;
|
||||
use App\Filament\Admin\Resources\SectionTemplates\Pages\EditSectionTemplate;
|
||||
use App\Filament\Admin\Resources\SectionTemplates\Pages\ListSectionTemplates;
|
||||
use App\Filament\Admin\Resources\SectionTemplates\Pages\ViewSectionTemplate;
|
||||
use App\Filament\Admin\Resources\SectionTemplates\Schemas\SectionTemplateForm;
|
||||
use App\Filament\Admin\Resources\SectionTemplates\Schemas\SectionTemplateInfolist;
|
||||
use App\Filament\Admin\Resources\SectionTemplates\Tables\SectionTemplatesTable;
|
||||
use App\Models\SectionTemplate;
|
||||
use BackedEnum;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Icons\Heroicon;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class SectionTemplateResource extends Resource
|
||||
{
|
||||
protected static ?string $model = SectionTemplate::class;
|
||||
|
||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
{
|
||||
return __('section-templates.navigation_group');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('section-templates.navigation_label');
|
||||
}
|
||||
|
||||
public static function getModelLabel(): string
|
||||
{
|
||||
return __('section-templates.model_label');
|
||||
}
|
||||
|
||||
public static function getPluralModelLabel(): string
|
||||
{
|
||||
return __('section-templates.plural_model_label');
|
||||
}
|
||||
|
||||
public static function form(Schema $schema): Schema
|
||||
{
|
||||
return SectionTemplateForm::configure($schema);
|
||||
}
|
||||
|
||||
public static function infolist(Schema $schema): Schema
|
||||
{
|
||||
return SectionTemplateInfolist::configure($schema);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return SectionTemplatesTable::configure($table);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\SectionTemplates\Tables;
|
||||
|
||||
use Filament\Actions\BulkActionGroup;
|
||||
use Filament\Actions\DeleteBulkAction;
|
||||
use Filament\Actions\EditAction;
|
||||
use Filament\Actions\ForceDeleteBulkAction;
|
||||
use Filament\Actions\RestoreBulkAction;
|
||||
use Filament\Actions\ViewAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\TrashedFilter;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
class SectionTemplatesTable
|
||||
{
|
||||
public static function configure(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->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(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Widgets;
|
||||
|
||||
use App\Filament\Admin\Resources\Pages\PageResource;
|
||||
use App\Models\Page;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use SolutionForest\FilamentTree\Actions\EditAction;
|
||||
use SolutionForest\FilamentTree\Actions\ViewAction;
|
||||
use SolutionForest\FilamentTree\Widgets\Tree as BaseWidget;
|
||||
|
||||
class PagesMenuWidget extends BaseWidget
|
||||
{
|
||||
protected static string $model = Page::class;
|
||||
|
||||
protected static int $maxDepth = 4;
|
||||
|
||||
protected bool $enableTreeTitle = true;
|
||||
|
||||
public function getTreeTitle(): string
|
||||
{
|
||||
return __('pages.menu_tree_title');
|
||||
}
|
||||
|
||||
protected function getTreeQuery(): Builder
|
||||
{
|
||||
return Page::query()
|
||||
->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,10 +181,7 @@ if (!function_exists('switch_language')) {
|
||||
}
|
||||
|
||||
app()->setLocale($languageCode);
|
||||
|
||||
if (auth()->check()) {
|
||||
session(['locale' => $languageCode]);
|
||||
}
|
||||
session(['locale' => $languageCode]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\Post;
|
||||
|
||||
class BlogController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$settings = class_exists(Setting::class) ? (Setting::query()->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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Models\SectionTemplate;
|
||||
use App\Models\MenuTemplate;
|
||||
use App\Services\TemplatePreviewService;
|
||||
use App\Services\TemplateService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TemplatePreviewController extends Controller
|
||||
{
|
||||
/**
|
||||
* Render template preview
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function preview(Request $request)
|
||||
{
|
||||
$recordId = $request->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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SetLocale
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Session'dan locale'i al, yoksa varsayılan dil kodunu kullan
|
||||
$locale = session('locale');
|
||||
|
||||
// Eğer session'da locale yoksa, varsayılan dil kodunu kullan
|
||||
if (!$locale) {
|
||||
if (function_exists('default_language_code')) {
|
||||
$locale = default_language_code();
|
||||
} else {
|
||||
$locale = config('app.locale');
|
||||
}
|
||||
}
|
||||
|
||||
// Aktif dilleri veritabanından kontrol et
|
||||
if (function_exists('available_language_codes')) {
|
||||
$availableLocales = available_language_codes();
|
||||
} else {
|
||||
$availableLocales = ['tr', 'en'];
|
||||
}
|
||||
|
||||
// Locale'i aktif diller arasında kontrol et
|
||||
if (in_array($locale, $availableLocales)) {
|
||||
App::setLocale($locale);
|
||||
} else {
|
||||
// Geçersiz locale ise varsayılan locale'e dön
|
||||
if (function_exists('default_language_code')) {
|
||||
App::setLocale(default_language_code());
|
||||
} else {
|
||||
App::setLocale(config('app.locale'));
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class FooterTemplate extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'html_content',
|
||||
'default_data',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'default_data' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the pages that use this footer template.
|
||||
*/
|
||||
public function pages(): HasMany
|
||||
{
|
||||
return $this->hasMany(Page::class, 'footer_template_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class HeaderTemplate extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'html_content',
|
||||
'default_data',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'default_data' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the pages that use this header template.
|
||||
*/
|
||||
public function pages(): HasMany
|
||||
{
|
||||
return $this->hasMany(Page::class, 'header_template_id');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class MenuTemplate extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'html_content',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
+113
-1
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class SectionTemplate extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'html_content',
|
||||
'default_data',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => '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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\Page;
|
||||
|
||||
class PageObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Page "saving" event.
|
||||
*
|
||||
* Bir sayfa homepage olarak işaretlendiğinde,
|
||||
* diğer tüm sayfaların homepage işaretini kaldır.
|
||||
*/
|
||||
public function saving(Page $page): void
|
||||
{
|
||||
// Eğer bu sayfa homepage olarak işaretleniyorsa
|
||||
if ($page->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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class GuideService
|
||||
{
|
||||
/**
|
||||
* Guide dosyalarının bulunduğu dizin
|
||||
*/
|
||||
protected string $guidePath;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Page;
|
||||
use App\Models\MenuTemplate;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class MenuService
|
||||
{
|
||||
/**
|
||||
* Render menu structure as HTML
|
||||
*
|
||||
* @return string Rendered menu HTML
|
||||
*/
|
||||
public static function render(): string
|
||||
{
|
||||
// Get menu items from Pages
|
||||
$menuItems = Cache::remember('menu_items', 3600, function () {
|
||||
return Page::with(['children' => 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 = '<ul class="menu">';
|
||||
|
||||
foreach ($menuItems as $item) {
|
||||
$hasChildren = $item->children->isNotEmpty();
|
||||
|
||||
$html .= '<li class="menu-item">';
|
||||
$html .= '<a href="' . route('page.show', $item->slug) . '" class="menu-link">';
|
||||
$html .= e($item->title);
|
||||
$html .= '</a>';
|
||||
|
||||
if ($hasChildren) {
|
||||
$html .= self::buildSubmenu($item->children);
|
||||
}
|
||||
|
||||
$html .= '</li>';
|
||||
}
|
||||
|
||||
$html .= '</ul>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build submenu structure recursively
|
||||
*
|
||||
* @param \Illuminate\Support\Collection $children
|
||||
* @return string Submenu HTML
|
||||
*/
|
||||
protected static function buildSubmenu($children): string
|
||||
{
|
||||
$html = '<ul class="submenu">';
|
||||
|
||||
foreach ($children as $child) {
|
||||
$hasChildren = $child->children->isNotEmpty();
|
||||
|
||||
$html .= '<li class="submenu-item">';
|
||||
$html .= '<a href="' . route('page.show', $child->slug) . '" class="submenu-link">';
|
||||
$html .= e($child->title);
|
||||
$html .= '</a>';
|
||||
|
||||
if ($hasChildren) {
|
||||
$html .= self::buildSubmenu($child->children);
|
||||
}
|
||||
|
||||
$html .= '</li>';
|
||||
}
|
||||
|
||||
$html .= '</ul>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear menu cache
|
||||
*/
|
||||
public static function clearCache(): void
|
||||
{
|
||||
Cache::forget('menu_items');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Setting;
|
||||
|
||||
class TemplatePreviewService
|
||||
{
|
||||
/**
|
||||
* Get all CSS assets for preview
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getCssAssets(): array
|
||||
{
|
||||
$config = config('template-preview.css', []);
|
||||
|
||||
if (config('template-preview.use_settings_override', false)) {
|
||||
$settingsKeys = config('template-preview.settings_keys', []);
|
||||
|
||||
foreach ($settingsKeys as $assetKey => $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(
|
||||
'<link rel="preload" href="%s" as="style" onload="this.rel=\'stylesheet\'">' . "\n",
|
||||
asset($path)
|
||||
);
|
||||
} else {
|
||||
$html .= sprintf(
|
||||
'<link rel="stylesheet" href="%s">' . "\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(
|
||||
'<script src="%s"></script>' . "\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' => '<div class="container py-8">' . $content . '</div>',
|
||||
default => $content,
|
||||
};
|
||||
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="{$charset}">
|
||||
<meta name="viewport" content="{$viewport}">
|
||||
<title>Template Preview</title>
|
||||
<base href="{$baseUrl}">
|
||||
{$cssLinks}
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0;">
|
||||
{$bodyContent}
|
||||
{$jsScripts}
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\HeaderTemplate;
|
||||
use App\Models\SectionTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use Filament\Forms\Components\{
|
||||
TextInput, Textarea, Select, Checkbox, CheckboxList,
|
||||
Radio, Toggle, ToggleButtons, DateTimePicker, DatePicker,
|
||||
TimePicker, FileUpload, RichEditor, MarkdownEditor,
|
||||
ColorPicker, TagsInput, KeyValue, CodeEditor, Hidden, Slider
|
||||
};
|
||||
|
||||
class TemplateService
|
||||
{
|
||||
/**
|
||||
* Generate dynamic form fields based on template placeholders
|
||||
* For default data (template forms), use dataKey = 'default_data'
|
||||
* For page data (page forms), use dataKey = 'header_data' or 'footer_data'
|
||||
*
|
||||
* @param array|null $defaultData Template's default_data array
|
||||
*/
|
||||
public static function generateDynamicFields($template, string $dataKey, ?array $existingData = null, ?array $defaultData = null): array
|
||||
{
|
||||
if (!$template) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$placeholders = self::parsePlaceholders($template->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 = "<div style='padding: 10px; background: #fee; border: 1px solid #fcc; color: #c00; margin: 5px 0; border-radius: 4px;'>" .
|
||||
"<strong>Custom Component Error:</strong> {$viewPath}<br>" .
|
||||
"<small>{$errorMessage}</small>" .
|
||||
"</div>";
|
||||
$html = str_replace($fullMatch, $errorHtml, $html);
|
||||
}
|
||||
} else {
|
||||
// Component doesn't exist - show notice in preview
|
||||
$noticeHtml = "<div style='padding: 10px; background: #fff3cd; border: 1px solid #ffc107; color: #856404; margin: 5px 0; border-radius: 4px;'>" .
|
||||
"<strong>Custom Component Not Found:</strong> {$viewPath}<br>" .
|
||||
"<small>Create the file at: resources/views/components/custom/{$componentName}.blade.php</small>" .
|
||||
"</div>";
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use App\Models\Page;
|
||||
use App\Services\TemplateService;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\View\Component;
|
||||
|
||||
class PageRenderer extends Component
|
||||
{
|
||||
public Page $page;
|
||||
|
||||
/**
|
||||
* Create a new component instance.
|
||||
*/
|
||||
public function __construct(Page $page)
|
||||
{
|
||||
$this->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
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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 {
|
||||
//
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+72
-1
@@ -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",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Template Select Options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file contains the options for select, multiselect, checkboxlist,
|
||||
| radio, and togglebuttons fields in dynamic templates.
|
||||
|
|
||||
| Format: 'field_name' => ['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
|
||||
];
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Template Preview Asset Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This configuration file contains all asset paths that should be loaded
|
||||
| in the template preview iframe. These assets are used to ensure the
|
||||
| preview displays correctly with the same styling and scripts as the
|
||||
| actual site.
|
||||
|
|
||||
| You can override these values in Settings table if needed, or modify
|
||||
| this config file for project-specific asset paths.
|
||||
|
|
||||
*/
|
||||
|
||||
// CSS Assets (Stylesheets)
|
||||
'css' => [
|
||||
'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',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
// JSON kolonu: Sayfa blokları (hero, features, pricing, cta vb.)
|
||||
$table->json('sections')->nullable()->after('content');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->dropColumn('sections');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('header_templates', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('section_templates', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('footer_templates', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('pages', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('header_templates', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('menu_templates', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Page;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PageSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// İlk kullanıcıyı bul veya oluştur (author_id için gerekli)
|
||||
$author = \App\Models\User::first();
|
||||
if (!$author) {
|
||||
$author = \App\Models\User::create([
|
||||
'name' => '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ı <span class="text-[#e31e24]">Web Çözümleri</span>',
|
||||
'subtitle' => 'Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz. Projelerinizi en son teknolojilerle hayata geçiriyoruz.',
|
||||
'primary_button_text' => 'Hemen Başla',
|
||||
'primary_button_url' => '#features',
|
||||
'secondary_button_text' => 'Daha Fazla Bilgi',
|
||||
'secondary_button_url' => '/about',
|
||||
'hero_image' => 'assets/img/demos/f1.png',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'section_badge' => 'ÖZELLİKLER',
|
||||
'section_title' => 'Neden Truncgil Citrus?',
|
||||
'section_subtitle' => 'Modern teknolojiler ve uzman ekibimizle projelerinizi hayata geçiriyoruz',
|
||||
'column_class' => 'md:w-6/12 lg:w-4/12',
|
||||
'features' => [
|
||||
[
|
||||
'icon' => 'assets/img/demos/fi1.png',
|
||||
'title' => 'Hızlı Çözümler',
|
||||
'description' => 'Modern teknolojiler kullanarak projelerinizi hızlı ve verimli bir şekilde hayata geçiriyoruz.',
|
||||
],
|
||||
[
|
||||
'icon' => 'assets/img/demos/fi2.png',
|
||||
'title' => 'Güvenli Altyapı',
|
||||
'description' => 'En son güvenlik standartlarını kullanarak verilerinizi koruma altına alıyoruz.',
|
||||
],
|
||||
[
|
||||
'icon' => 'assets/img/demos/fi3.png',
|
||||
'title' => 'Uzman Ekip',
|
||||
'description' => 'Deneyimli ve uzman ekibimiz ile her zaman yanınızdayız.',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'stats',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#ffffff]',
|
||||
'column_class' => 'md:w-6/12 lg:w-3/12',
|
||||
'stats' => [
|
||||
['number' => '500+', 'label' => 'Tamamlanan Proje'],
|
||||
['number' => '300+', 'label' => 'Mutlu Müşteri'],
|
||||
['number' => '50+', 'label' => 'Kazanılan Ödül'],
|
||||
['number' => '25+', 'label' => 'Ekip Üyesi'],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'cta',
|
||||
'data' => [
|
||||
'bg_class' => 'overflow-hidden',
|
||||
'background_image' => 'assets/img/photos/blurry.png',
|
||||
'icon' => 'assets/img/demos/icon-grape.png',
|
||||
'title' => 'Benzersiz düşünün ve <span class="text-[#e31e24]">fark yaratın</span>',
|
||||
'subtitle' => 'Binlerce müşterimiz tarafından güveniliyoruz. Siz de katılın ve projelerinizi hayata geçirin.',
|
||||
'button_text' => 'İletişime Geç',
|
||||
'button_url' => '/contact',
|
||||
'button_icon' => 'uil uil-arrow-up-right',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// About Page
|
||||
[
|
||||
'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 <span class="text-[#e31e24]">Çözümleri</span>',
|
||||
'subtitle' => '2010 yılından bu yana dijital dönüşüm süreçlerinde lider konumdayız',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#ffffff]',
|
||||
'section_title' => 'Değerlerimiz',
|
||||
'section_subtitle' => 'İş süreçlerimizi şekillendiren temel prensiplerimiz',
|
||||
'column_class' => 'md:w-6/12 lg:w-3/12',
|
||||
'features' => [
|
||||
['icon' => 'assets/img/demos/fi1.png', 'title' => 'İnovasyon', 'description' => 'Sürekli yenilik ve gelişim'],
|
||||
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Kalite', 'description' => 'En yüksek standartlarda hizmet'],
|
||||
['icon' => 'assets/img/demos/fi3.png', 'title' => 'Güvenilirlik', 'description' => 'Zamanında ve eksiksiz teslimat'],
|
||||
['icon' => 'assets/img/demos/fi4.png', 'title' => 'Destek', 'description' => '7/24 müşteri desteği'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// Services Page
|
||||
[
|
||||
'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 <span class="text-[#e31e24]">Çözümleri</span>',
|
||||
'subtitle' => 'İşinizi bir üst seviyeye taşıyacak teknoloji hizmetlerimiz',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'section_title' => 'Sunduğumuz Hizmetler',
|
||||
'column_class' => 'md:w-6/12 lg:w-4/12',
|
||||
'features' => [
|
||||
['icon' => 'assets/img/demos/fi1.png', 'title' => 'Web Tasarım & Geliştirme', 'description' => 'Modern, responsive ve SEO uyumlu web siteleri'],
|
||||
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Mobil Uygulama', 'description' => 'iOS ve Android için native ve cross-platform uygulamalar'],
|
||||
['icon' => 'assets/img/demos/fi3.png', 'title' => 'E-Ticaret Çözümleri', 'description' => 'Entegre ödeme sistemleri ile online satış platformları'],
|
||||
['icon' => 'assets/img/demos/fi4.png', 'title' => 'Dijital Pazarlama', 'description' => 'SEO, SEM ve sosyal medya yönetimi'],
|
||||
['icon' => 'assets/img/demos/fi1.png', 'title' => 'Kurumsal Yazılım', 'description' => 'İşletmenize özel CRM, ERP ve özel yazılım çözümleri'],
|
||||
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Bulut Hizmetleri', 'description' => 'Güvenli ve ölçeklenebilir bulut altyapısı'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// Contact Page
|
||||
[
|
||||
'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 <span class="text-[#e31e24]">Konuşalım</span>',
|
||||
'subtitle' => 'Size özel çözümler geliştirmek için bizimle iletişime geçin',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'contact',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'title' => 'Bizimle İletişime Geçin',
|
||||
'subtitle' => 'Projeleriniz hakkında konuşmak için bize ulaşın',
|
||||
'info' => [
|
||||
'address' => 'Merkez Mah. Teknoloji Cad. No:123 İstanbul',
|
||||
'phone' => '+90 (212) 555 1234',
|
||||
'email' => 'info@truncgil.com',
|
||||
],
|
||||
'form_action' => '/contact/submit',
|
||||
'button_text' => 'Gönder',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// Pricing Page (örnek)
|
||||
[
|
||||
'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 <span class="text-[#e31e24]">Paketi Seçin</span>',
|
||||
'subtitle' => 'Tüm paketler 30 gün para iade garantisi ile geliyor',
|
||||
],
|
||||
],
|
||||
[
|
||||
'type' => 'pricing',
|
||||
'data' => [
|
||||
'bg_class' => '!bg-[#f0f0f8]',
|
||||
'section_title' => 'Web Tasarım Paketleri',
|
||||
'column_class' => 'md:w-6/12 lg:w-4/12',
|
||||
'featured_label' => 'Önerilen',
|
||||
'plans' => [
|
||||
[
|
||||
'name' => 'Başlangıç',
|
||||
'currency' => '₺',
|
||||
'price' => '999',
|
||||
'period' => 'ay',
|
||||
'features' => ['5 Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', '7/24 Destek'],
|
||||
'button_text' => 'Başla',
|
||||
'button_url' => '/contact',
|
||||
],
|
||||
[
|
||||
'name' => 'Profesyonel',
|
||||
'currency' => '₺',
|
||||
'price' => '2499',
|
||||
'period' => 'ay',
|
||||
'featured' => true,
|
||||
'features' => ['15 Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', 'Yönetim Paneli', '7/24 Öncelikli Destek'],
|
||||
'button_text' => 'Başla',
|
||||
'button_url' => '/contact',
|
||||
],
|
||||
[
|
||||
'name' => 'Kurumsal',
|
||||
'currency' => '₺',
|
||||
'price' => '4999',
|
||||
'period' => 'ay',
|
||||
'features' => ['Sınırsız Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', 'Gelişmiş Yönetim Paneli', 'Özel Entegrasyonlar', '7/24 Öncelikli Destek'],
|
||||
'button_text' => 'Başla',
|
||||
'button_url' => '/contact',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($pages as $pageData) {
|
||||
Page::updateOrCreate(
|
||||
['slug' => $pageData['slug']],
|
||||
$pageData
|
||||
);
|
||||
}
|
||||
|
||||
$this->command->info('✅ ' . count($pages) . ' sayfa başarıyla oluşturuldu!');
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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ı <span class='text-[#e31e24]'>Web Çözümleri</span>",
|
||||
"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 <span class='text-[#e31e24]'>fark yaratın</span>",
|
||||
"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') }}
|
||||
|
||||
@@ -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
|
||||
<header class="site-header" style="background-color: {color.header_bg};">
|
||||
<div class="container">
|
||||
<!-- Logo Section -->
|
||||
<div class="logo">
|
||||
<img src="{image.logo}" alt="{text.company_name}">
|
||||
<span class="tagline">{textarea.tagline}</span>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="main-nav">
|
||||
{richtext.menu_html}
|
||||
</nav>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="contact-info">
|
||||
<a href="mailto:{email.contact_email}">{email.contact_email}</a>
|
||||
<a href="tel:{tel.phone}">{tel.phone}</a>
|
||||
</div>
|
||||
|
||||
<!-- Social Links -->
|
||||
<div class="social-links">
|
||||
<a href="{url.facebook_link}" target="_blank">Facebook</a>
|
||||
<a href="{url.twitter_link}" target="_blank">Twitter</a>
|
||||
<a href="{url.linkedin_link}" target="_blank">LinkedIn</a>
|
||||
</div>
|
||||
|
||||
<!-- Toggle Features -->
|
||||
<div class="features">
|
||||
{toggle.show_search}
|
||||
{toggle.show_cart}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- CSS Snippet -->
|
||||
<style>
|
||||
.site-header {
|
||||
padding: {number.padding}px;
|
||||
opacity: {slider.opacity}%;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
**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
|
||||
<!-- resources/views/components/page-renderer.blade.php -->
|
||||
<div class="page-wrapper">
|
||||
{!! $renderHeader() !!}
|
||||
|
||||
<main class="page-content">
|
||||
{!! $renderSections() !!}
|
||||
</main>
|
||||
|
||||
{!! $renderFooter() !!}
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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
|
||||
<header>
|
||||
<img src="{image.logo}" alt="{text.site_name}">
|
||||
<nav>{richtext.menu_html}</nav>
|
||||
<a href="mailto:{email.contact}">{email.contact}</a>
|
||||
</header>
|
||||
```
|
||||
|
||||
### 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
|
||||
<x-page-renderer :page="$page" />
|
||||
```
|
||||
|
||||
Output:
|
||||
```html
|
||||
<header>
|
||||
<img src="/storage/uploads/logo.png" alt="My Company">
|
||||
<nav><ul><li><a href="/">Home</a></li></ul></nav>
|
||||
<a href="mailto:info@company.com">info@company.com</a>
|
||||
</header>
|
||||
```
|
||||
|
||||
## 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
|
||||
<nav class="navbar">
|
||||
<div class="menu-container">
|
||||
{menu}
|
||||
</div>
|
||||
</nav>
|
||||
```
|
||||
|
||||
#### 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
|
||||
<header class="site-header">
|
||||
<div class="logo">
|
||||
<img src="{image.logo}" alt="{text.site_name}">
|
||||
</div>
|
||||
<nav class="main-nav">
|
||||
{menu}
|
||||
</nav>
|
||||
</header>
|
||||
```
|
||||
|
||||
**Çıktı:**
|
||||
```html
|
||||
<header class="site-header">
|
||||
<div class="logo">
|
||||
<img src="/storage/logo.png" alt="My Site">
|
||||
</div>
|
||||
<nav class="main-nav">
|
||||
<ul class="menu">
|
||||
<li class="menu-item">
|
||||
<a href="/home" class="menu-link">Ana Sayfa</a>
|
||||
</li>
|
||||
<li class="menu-item">
|
||||
<a href="/about" class="menu-link">Hakkımızda</a>
|
||||
<ul class="submenu">
|
||||
<li class="submenu-item">
|
||||
<a href="/about/team" class="submenu-link">Ekibimiz</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
# Page Section Builder - Kullanım Kılavuzu
|
||||
|
||||
## 📋 Genel Bakış
|
||||
|
||||
Page Section Builder, Filament Admin panelinden sayfalarınıza dinamik bölümler (sections) eklemenizi sağlayan güçlü bir modüldür. Settings modülünden esinlenerek geliştirilmiş olup, esnek bir key-value-type sistemi kullanır.
|
||||
|
||||
## ✨ Özellikler
|
||||
|
||||
- **Dinamik Section Tipleri**: Hero, Features, Stats, CTA, Gallery, Testimonials ve daha fazlası
|
||||
- **Esnek Data Yönetimi**: Her section için sınırsız key-value çifti
|
||||
- **Çoklu Value Tipleri**: Text, Image, HTML, Rich Text, Markdown, Color, Date, Array ve daha fazlası
|
||||
- **Sürükle-Bırak Sıralama**: Sections ve data fields'ları yeniden sıralayabilme
|
||||
- **Kolay Entegrasyon**: Mevcut sayfalarda sorunsuz çalışır
|
||||
|
||||
## 🎯 Section Tipleri
|
||||
|
||||
Aşağıdaki section tipleri mevcuttur:
|
||||
|
||||
| Tip | Açıklama | Kullanım Alanı |
|
||||
|-----|----------|----------------|
|
||||
| `hero` | Hero (Ana Banner) | Sayfa başlığı, büyük görsel banner |
|
||||
| `features` | Özellikler | Ürün/hizmet özellikleri listesi |
|
||||
| `stats` | İstatistikler | Sayısal veriler, başarı metrikleri |
|
||||
| `cta` | Harekete Geçirme | Kullanıcıyı yönlendirme butonları |
|
||||
| `content` | İçerik | Genel içerik blokları |
|
||||
| `gallery` | Galeri | Görsel galeri |
|
||||
| `testimonials` | Referanslar | Müşteri yorumları |
|
||||
| `team` | Ekip | Ekip üyeleri |
|
||||
| `pricing` | Fiyatlandırma | Fiyat tabloları |
|
||||
| `faq` | SSS | Sık sorulan sorular |
|
||||
| `contact` | İletişim | İletişim formu |
|
||||
| `custom` | Özel | Özel içerik |
|
||||
|
||||
## 📝 Value Tipleri
|
||||
|
||||
Her data field'ı için aşağıdaki value tipleri kullanılabilir:
|
||||
|
||||
### Metin Tipleri
|
||||
- **Text**: Kısa tek satırlık metin (başlık, URL, e-posta, telefon)
|
||||
- **Textarea**: Uzun metin
|
||||
- **HTML**: Ham HTML kodu
|
||||
- **Markdown**: Markdown formatı
|
||||
- **Rich Text**: WYSIWYG zengin metin editörü
|
||||
|
||||
### Medya Tipleri
|
||||
- **Image**: Görsel yükleme (otomatik image editor ile)
|
||||
- **File**: Dosya yükleme
|
||||
- **Color**: Renk seçici
|
||||
|
||||
### Veri Tipleri
|
||||
- **Number**: Sayısal değer
|
||||
- **Boolean**: Evet/Hayır (Toggle)
|
||||
- **Date**: Tarih seçici
|
||||
- **DateTime**: Tarih & saat seçici
|
||||
- **Array**: Dizi (repeater)
|
||||
- **JSON**: JSON formatı
|
||||
|
||||
## 🔧 Admin Panelinde Kullanım
|
||||
|
||||
### 1. Yeni Section Ekleme
|
||||
|
||||
1. **Pages** modülünden bir sayfa açın veya yeni sayfa oluşturun
|
||||
2. **Sayfa Bölümleri** (Page Sections) sekmesine gidin
|
||||
3. **Yeni Bölüm Ekle** butonuna tıklayın
|
||||
4. Section tipini seçin (örn: Hero, Features)
|
||||
5. **Yeni Alan Ekle** ile section data'larını ekleyin
|
||||
|
||||
### 2. Data Field Ekleme
|
||||
|
||||
Her section için data field'ları eklerken:
|
||||
|
||||
1. **Key (Anahtar)**: Data'nın anahtarı (örn: `title`, `subtitle`, `image`)
|
||||
2. **Type (Değer Tipi)**: Değerin tipini seçin (örn: Text, Image, HTML)
|
||||
3. **Value (Değer)**: Seçilen tipe göre değeri girin
|
||||
|
||||
### 3. Örnek: Hero Section Oluşturma
|
||||
|
||||
```
|
||||
Section Type: Hero (Ana Banner)
|
||||
|
||||
Data Fields:
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Key: background_image │
|
||||
│ Type: Image │
|
||||
│ Value: [Upload Image] │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: badge │
|
||||
│ Type: Text │
|
||||
│ Value: "YENİ PLATFORM" │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: title │
|
||||
│ Type: HTML │
|
||||
│ Value: "Modern <span>Web</span>" │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: subtitle │
|
||||
│ Type: Textarea │
|
||||
│ Value: "Açıklama metni..." │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: button_text │
|
||||
│ Type: Text │
|
||||
│ Value: "Hemen Başla" │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Key: button_url │
|
||||
│ Type: URL │
|
||||
│ Value: "#features" │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 💻 Kod Kullanımı
|
||||
|
||||
### Page Model'de Sections Kullanımı
|
||||
|
||||
```php
|
||||
// Bir sayfayı al
|
||||
$page = Page::find(1);
|
||||
|
||||
// Tüm parsed sections'ları al (key-value formatında)
|
||||
$sections = $page->parsed_sections;
|
||||
|
||||
// Örnek çıktı:
|
||||
[
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'background_image' => 'path/to/image.jpg',
|
||||
'badge' => 'YENİ PLATFORM',
|
||||
'title' => 'Modern <span>Web</span>',
|
||||
'subtitle' => 'Açıklama metni...',
|
||||
'button_text' => 'Hemen Başla',
|
||||
'button_url' => '#features',
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'features',
|
||||
'data' => [...]
|
||||
]
|
||||
]
|
||||
|
||||
// Belirli bir section tipinin data'sını al
|
||||
$heroData = $page->getSectionData('hero'); // İlk hero section
|
||||
$secondHero = $page->getSectionData('hero', 1); // İkinci hero section
|
||||
```
|
||||
|
||||
### Controller'da Kullanım
|
||||
|
||||
```php
|
||||
public function show($slug)
|
||||
{
|
||||
$page = Page::where('slug', $slug)->firstOrFail();
|
||||
|
||||
// Parsed sections otomatik olarak view'e gönderilir
|
||||
$sections = $page->parsed_sections;
|
||||
|
||||
return view('templates.home', compact('page', 'sections'));
|
||||
}
|
||||
```
|
||||
|
||||
### Blade Template'de Kullanım
|
||||
|
||||
```blade
|
||||
@foreach($sections as $section)
|
||||
@if($section['type'] === 'hero')
|
||||
<x-blocks.hero :data="$section['data']" />
|
||||
|
||||
@elseif($section['type'] === 'features')
|
||||
<x-blocks.features :data="$section['data']" />
|
||||
|
||||
@elseif($section['type'] === 'stats')
|
||||
<x-blocks.stats :data="$section['data']" />
|
||||
|
||||
@endif
|
||||
@endforeach
|
||||
```
|
||||
|
||||
### Component'te Data Kullanımı
|
||||
|
||||
```blade
|
||||
{{-- resources/views/components/blocks/hero.blade.php --}}
|
||||
|
||||
@props(['data'])
|
||||
|
||||
<section style="background-image: url({{ asset($data['background_image'] ?? '') }})">
|
||||
<div class="container">
|
||||
@if(isset($data['badge']))
|
||||
<span class="badge">{{ $data['badge'] }}</span>
|
||||
@endif
|
||||
|
||||
@if(isset($data['title']))
|
||||
<h1>{!! $data['title'] !!}</h1>
|
||||
@endif
|
||||
|
||||
@if(isset($data['subtitle']))
|
||||
<p>{{ $data['subtitle'] }}</p>
|
||||
@endif
|
||||
|
||||
@if(isset($data['button_text']) && isset($data['button_url']))
|
||||
<a href="{{ $data['button_url'] }}" class="btn">
|
||||
{{ $data['button_text'] }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
## 🎨 Best Practices
|
||||
|
||||
### 1. Key Naming (Anahtar İsimlendirme)
|
||||
|
||||
Tutarlı ve açıklayıcı key isimleri kullanın:
|
||||
|
||||
```
|
||||
✅ İYİ:
|
||||
- title
|
||||
- subtitle
|
||||
- background_image
|
||||
- primary_button_text
|
||||
- primary_button_url
|
||||
|
||||
❌ KÖTÜ:
|
||||
- baslik
|
||||
- resim1
|
||||
- text
|
||||
- btn
|
||||
```
|
||||
|
||||
### 2. Value Type Seçimi
|
||||
|
||||
Doğru value tipini seçin:
|
||||
|
||||
```
|
||||
✅ title → Text (kısa başlık)
|
||||
✅ description → Textarea (uzun açıklama)
|
||||
✅ content → Rich Text (formatlanmış içerik)
|
||||
✅ banner → Image (görsel)
|
||||
✅ is_active → Boolean (aktif/pasif)
|
||||
```
|
||||
|
||||
### 3. Section Organization
|
||||
|
||||
Benzer section'ları gruplandırın:
|
||||
|
||||
```
|
||||
1. Hero (Giriş)
|
||||
2. Features (Özellikler)
|
||||
3. Stats (İstatistikler)
|
||||
4. CTA (Harekete Geçirme)
|
||||
5. Contact (İletişim)
|
||||
```
|
||||
|
||||
## 🔄 Mevcut Data Migrasyon
|
||||
|
||||
Eğer eski formatınız varsa (örn: PageSeeder'daki gibi), parse edilmiş format otomatik olarak çalışır:
|
||||
|
||||
```php
|
||||
// ESKİ FORMAT (PageSeeder'dan)
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
'title' => 'Başlık',
|
||||
'subtitle' => 'Alt başlık'
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
// YENİ FORMAT (Admin'den girilince)
|
||||
'sections' => [
|
||||
[
|
||||
'type' => 'hero',
|
||||
'data' => [
|
||||
['key' => 'title', 'type' => 'text', 'value' => 'Başlık'],
|
||||
['key' => 'subtitle', 'type' => 'text', 'value' => 'Alt başlık']
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
// parsed_sections attribute her ikisini de doğru parse eder!
|
||||
```
|
||||
|
||||
## 🚀 İleri Seviye Özellikler
|
||||
|
||||
### Nested Arrays (İç İçe Diziler)
|
||||
|
||||
`Array` value tipi ile iç içe veri yapıları oluşturabilirsiniz:
|
||||
|
||||
```
|
||||
Section Type: Features
|
||||
|
||||
Data Field:
|
||||
Key: features
|
||||
Type: Array
|
||||
Value: [
|
||||
{ item: "Hızlı Çözümler" },
|
||||
{ item: "Güvenli Altyapı" },
|
||||
{ item: "Uzman Ekip" }
|
||||
]
|
||||
```
|
||||
|
||||
### JSON Data
|
||||
|
||||
Kompleks veri yapıları için JSON kullanabilirsiniz:
|
||||
|
||||
```
|
||||
Key: configuration
|
||||
Type: JSON
|
||||
Value: {"layout": "grid", "columns": 3, "gap": 20}
|
||||
```
|
||||
|
||||
### Conditional Rendering
|
||||
|
||||
```blade
|
||||
@foreach($sections as $section)
|
||||
@php
|
||||
$data = $section['data'];
|
||||
$bgClass = $data['bg_class'] ?? '';
|
||||
@endphp
|
||||
|
||||
<section class="{{ $bgClass }}">
|
||||
@includeIf("components.blocks.{$section['type']}", compact('data'))
|
||||
</section>
|
||||
@endforeach
|
||||
```
|
||||
|
||||
## 📚 Dosya Yapısı
|
||||
|
||||
```
|
||||
app/
|
||||
├── Models/
|
||||
│ └── Page.php (getParsedSectionsAttribute, getSectionData)
|
||||
├── Http/Controllers/
|
||||
│ └── PageController.php (parsed_sections kullanımı)
|
||||
└── Filament/Admin/Resources/Pages/
|
||||
└── Schemas/
|
||||
└── PageForm.php (Section Builder UI)
|
||||
|
||||
lang/
|
||||
├── tr/
|
||||
│ └── pages.php (Türkçe çeviriler)
|
||||
└── en/
|
||||
└── pages.php (İngilizce çeviriler)
|
||||
|
||||
resources/views/
|
||||
├── components/blocks/
|
||||
│ ├── hero.blade.php
|
||||
│ ├── features.blade.php
|
||||
│ ├── stats.blade.php
|
||||
│ └── cta.blade.php
|
||||
└── templates/
|
||||
└── home.blade.php (sections render)
|
||||
```
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Section görünmüyor?
|
||||
|
||||
1. `$page->parsed_sections` döndüğünden emin olun
|
||||
2. Section type'ının doğru yazıldığını kontrol edin
|
||||
3. Blade component'in var olduğunu doğrulayın
|
||||
|
||||
### Data değerleri boş?
|
||||
|
||||
1. Admin'de data field'larının key değerlerini kontrol edin
|
||||
2. Value tipinin doğru seçildiğini doğrulayın
|
||||
3. Değerlerin kaydedildiğini kontrol edin
|
||||
|
||||
### Image görünmüyor?
|
||||
|
||||
1. `storage` link'inin oluşturulduğunu kontrol edin: `php artisan storage:link`
|
||||
2. Image path'inin doğru olduğunu doğrulayın
|
||||
3. Disk ayarlarını kontrol edin (`config/filesystems.php`)
|
||||
|
||||
## 🎓 Örnekler
|
||||
|
||||
Daha fazla örnek için:
|
||||
- `database/seeders/PageSeeder.php` - Mevcut section örnekleri
|
||||
- `resources/views/components/blocks/` - Component örnekleri
|
||||
- `resources/views/templates/home.blade.php` - Template örneği
|
||||
|
||||
## 📞 Destek
|
||||
|
||||
Sorunlarınız için:
|
||||
1. Bu dokümantasyonu kontrol edin
|
||||
2. `docs/CURSOR_RULES.md` dosyasına bakın
|
||||
3. Loglara göz atın: `storage/logs/laravel.log`
|
||||
|
||||
---
|
||||
|
||||
**Not:** Bu sistem Settings modülünden esinlenerek geliştirilmiş olup, aynı esneklik ve güçle çalışır. Her türlü dinamik sayfa içeriği için kullanılabilir.
|
||||
|
||||
@@ -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') }}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation & list
|
||||
'nav-blog' => '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',
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation
|
||||
'navigation_group' => '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.',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation
|
||||
'navigation_group' => '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.',
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation
|
||||
'navigation_group' => '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.',
|
||||
];
|
||||
|
||||
+81
-8
@@ -1,6 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation
|
||||
'nav-home' => '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',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => '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',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation
|
||||
'navigation_group' => '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.',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => '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.',
|
||||
];
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation & list
|
||||
'nav-blog' => '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',
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Navigation
|
||||
'navigation_group' => 'Ş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.',
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user