Add dynamic template system: Implemented header, footer, and section templates with corresponding models, migrations, and Filament resources. Enhanced Page model to support dynamic templates and updated localization files for new terms. Added TemplateService for managing template data and rendering. Comprehensive documentation included for usage and best practices.

This commit is contained in:
Ümit Tunç
2025-10-31 14:53:34 -03:00
parent 0f78292c46
commit c042cb5114
46 changed files with 2709 additions and 0 deletions
@@ -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,11 @@
<?php
namespace App\Filament\Admin\Resources\FooterTemplates\Pages;
use App\Filament\Admin\Resources\FooterTemplates\FooterTemplateResource;
use Filament\Resources\Pages\CreateRecord;
class CreateFooterTemplate extends CreateRecord
{
protected static string $resource = FooterTemplateResource::class;
}
@@ -0,0 +1,25 @@
<?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;
class EditFooterTemplate extends EditRecord
{
protected static string $resource = FooterTemplateResource::class;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}
@@ -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,40 @@
<?php
namespace App\Filament\Admin\Resources\FooterTemplates\Schemas;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
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(),
CodeEditor::make('html_content')
->label(__('footer-templates.field_html_content'))
->required()
->lineNumbers()
->columnSpanFull()
->helperText(__('footer-templates.field_html_content_help')),
Toggle::make('is_active')
->label(__('footer-templates.field_is_active'))
->default(true)
->inline(false),
])
->columns(2),
]);
}
}
@@ -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,11 @@
<?php
namespace App\Filament\Admin\Resources\HeaderTemplates\Pages;
use App\Filament\Admin\Resources\HeaderTemplates\HeaderTemplateResource;
use Filament\Resources\Pages\CreateRecord;
class CreateHeaderTemplate extends CreateRecord
{
protected static string $resource = HeaderTemplateResource::class;
}
@@ -0,0 +1,25 @@
<?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;
class EditHeaderTemplate extends EditRecord
{
protected static string $resource = HeaderTemplateResource::class;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}
@@ -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,40 @@
<?php
namespace App\Filament\Admin\Resources\HeaderTemplates\Schemas;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
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(),
CodeEditor::make('html_content')
->label(__('header-templates.field_html_content'))
->required()
->lineNumbers()
->columnSpanFull()
->helperText(__('header-templates.field_html_content_help')),
Toggle::make('is_active')
->label(__('header-templates.field_is_active'))
->default(true)
->inline(false),
])
->columns(2),
]);
}
}
@@ -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,10 @@
namespace App\Filament\Admin\Resources\Pages\Schemas; namespace App\Filament\Admin\Resources\Pages\Schemas;
use App\Filament\Admin\Resources\Components\TranslationTabs; use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Models\HeaderTemplate;
use App\Models\SectionTemplate;
use App\Models\FooterTemplate;
use App\Services\TemplateService;
use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor; use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\CodeEditor\Enums\Language; use Filament\Forms\Components\CodeEditor\Enums\Language;
@@ -19,7 +23,9 @@ use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle; use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
class PageForm class PageForm
@@ -527,6 +533,88 @@ class PageForm
->collapsible(true) ->collapsible(true)
->collapsed(false), ->collapsed(false),
// Dynamic Template System
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(fn (Set $set) => $set('header_data', []))
->columnSpanFull(),
Group::make()
->schema(fn (Get $get): array => TemplateService::generateDynamicFields(
HeaderTemplate::find($get('header_template_id')),
'header_data'
))
->visible(fn (Get $get): bool => filled($get('header_template_id')))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true),
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()
->columnSpanFull(),
Group::make()
->schema(fn (Get $get): array => TemplateService::generateDynamicFields(
SectionTemplate::find($get('section_template_id')),
'section_data'
))
->visible(fn (Get $get): bool => filled($get('section_template_id')))
->columnSpanFull(),
])
->defaultItems(0)
->addActionLabel(__('pages.add_template_section'))
->reorderable()
->collapsible()
->itemLabel(fn (array $state): ?string =>
SectionTemplate::find($state['section_template_id'] ?? null)?->title ?? __('pages.template_section')
)
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true),
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(fn (Set $set) => $set('footer_data', []))
->columnSpanFull(),
Group::make()
->schema(fn (Get $get): array => TemplateService::generateDynamicFields(
FooterTemplate::find($get('footer_template_id')),
'footer_data'
))
->visible(fn (Get $get): bool => filled($get('footer_template_id')))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true),
// Çeviri Sekmesi // Çeviri Sekmesi
Section::make('🌍 ' . __('pages.translations_section')) Section::make('🌍 ' . __('pages.translations_section'))
->schema([ ->schema([
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\SectionTemplates\Pages;
use App\Filament\Admin\Resources\SectionTemplates\SectionTemplateResource;
use Filament\Resources\Pages\CreateRecord;
class CreateSectionTemplate extends CreateRecord
{
protected static string $resource = SectionTemplateResource::class;
}
@@ -0,0 +1,25 @@
<?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;
class EditSectionTemplate extends EditRecord
{
protected static string $resource = SectionTemplateResource::class;
protected function getHeaderActions(): array
{
return [
ViewAction::make(),
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}
@@ -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,40 @@
<?php
namespace App\Filament\Admin\Resources\SectionTemplates\Schemas;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
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(),
CodeEditor::make('html_content')
->label(__('section-templates.field_html_content'))
->required()
->lineNumbers()
->columnSpanFull()
->helperText(__('section-templates.field_html_content_help')),
Toggle::make('is_active')
->label(__('section-templates.field_is_active'))
->default(true)
->inline(false),
])
->columns(2),
]);
}
}
@@ -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(),
]),
]);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Get the pages that use this footer template.
*/
public function pages(): HasMany
{
return $this->hasMany(Page::class, 'footer_template_id');
}
}
+30
View File
@@ -0,0 +1,30 @@
<?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',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* Get the pages that use this header template.
*/
public function pages(): HasMany
{
return $this->hasMany(Page::class, 'header_template_id');
}
}
+47
View File
@@ -29,6 +29,12 @@ class Page extends Model
'show_in_menu', 'show_in_menu',
'sections', 'sections',
'data', 'data',
// Template System
'header_template_id',
'header_data',
'footer_template_id',
'footer_data',
'sections_data',
]; ];
/** /**
@@ -49,6 +55,10 @@ class Page extends Model
'sort_order' => 'integer', 'sort_order' => 'integer',
'sections' => 'array', 'sections' => 'array',
'data' => 'array', 'data' => 'array',
// Template System
'header_data' => 'array',
'footer_data' => 'array',
'sections_data' => 'array',
]; ];
public function author() public function author()
@@ -66,6 +76,22 @@ class Page extends Model
return $this->hasMany(Page::class, 'parent_id'); 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() public function getRouteKeyName()
{ {
return 'slug'; return 'slug';
@@ -124,4 +150,25 @@ class Page extends Model
return $matchingSections[$index] ?? null; 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);
}
} }
+30
View File
@@ -0,0 +1,30 @@
<?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',
'is_active',
];
protected $casts = [
'is_active' => 'boolean',
];
/**
* 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();
}
}
+259
View File
@@ -0,0 +1,259 @@
<?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
*/
public static function generateDynamicFields($template, string $dataKey): 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;
$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;
}
/**
* Parse placeholders from HTML content
* Format: {type.field_name}
*/
public static function parsePlaceholders(string $html): array
{
preg_match_all('/\{([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
*/
public static function replacePlaceholders(string $html, array $data): string
{
foreach ($data as $placeholder => $value) {
// Handle different value types
if (is_array($value)) {
$value = implode(', ', $value);
} elseif (is_bool($value)) {
$value = $value ? 'true' : 'false';
}
$html = str_replace("{{$placeholder}}", $value ?? '', $html);
}
return $html;
}
}
+81
View File
@@ -0,0 +1,81 @@
<?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 '';
}
return TemplateService::replacePlaceholders(
$this->page->headerTemplate->html_content,
$this->page->header_data ?? []
);
}
/**
* Render all sections with their templates
*/
public function renderSections(): string
{
$output = '';
foreach ($this->page->templated_sections as $section) {
if (!$section['template']) {
continue;
}
$output .= TemplateService::replacePlaceholders(
$section['template']->html_content,
$section['data']
);
}
return $output;
}
/**
* Render the footer template with data
*/
public function renderFooter(): string
{
if (!$this->page->footerTemplate) {
return '';
}
return TemplateService::replacePlaceholders(
$this->page->footerTemplate->html_content,
$this->page->footer_data ?? []
);
}
}
+43
View File
@@ -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,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');
});
}
};
+796
View File
@@ -0,0 +1,796 @@
# 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`
**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}
```
**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
```
## İmplement Adımları
1. ✅ Migrations oluştur (4 adet)
2. ✅ Model'ler oluştur (3 adet)
3. ✅ Filament Resources oluştur (3 adet)
4. ✅ TemplateService helper oluştur
5. ✅ PageForm'u güncelle (template tabs ekle)
6. ✅ PageRenderer component oluştur
7. ✅ Lang dosyaları oluştur (6 adet)
8. ✅ Frontend route ve controller güncelle
9. ✅ Test
---
**Not:** Bu sistem mevcut `page_sections` sisteminin yerini alacak ve çok daha güçlü bir alternatif sunacaktır.
+41
View File
@@ -0,0 +1,41 @@
<?php
return [
// Navigation
'navigation_group' => 'Templates',
'navigation_label' => 'Footer Templates',
'model_label' => 'Footer Template',
'plural_model_label' => 'Footer Templates',
// Sections
'section_general' => 'General Information',
// 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.',
];
+41
View File
@@ -0,0 +1,41 @@
<?php
return [
// Navigation
'navigation_group' => 'Templates',
'navigation_label' => 'Header Templates',
'model_label' => 'Header Template',
'plural_model_label' => 'Header Templates',
// Sections
'section_general' => 'General Information',
// 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.',
];
+16
View File
@@ -147,4 +147,20 @@ return [
'value_type_datetime' => 'Date & Time', 'value_type_datetime' => 'Date & Time',
'value_type_array' => 'Array', 'value_type_array' => 'Array',
'value_type_json' => 'JSON', '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',
]; ];
+40
View File
@@ -0,0 +1,40 @@
<?php
return [
// Navigation
'navigation_group' => 'Templates',
'navigation_label' => 'Section Templates',
'model_label' => 'Section Template',
'plural_model_label' => 'Section Templates',
// Sections
'section_general' => 'General Information',
// 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.',
];
+41
View File
@@ -0,0 +1,41 @@
<?php
return [
// Navigation
'navigation_group' => 'Şablonlar',
'navigation_label' => 'Footer Şablonları',
'model_label' => 'Footer Şablonu',
'plural_model_label' => 'Footer Şablonları',
// Sections
'section_general' => 'Genel Bilgiler',
// 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.',
];
+41
View File
@@ -0,0 +1,41 @@
<?php
return [
// Navigation
'navigation_group' => 'Şablonlar',
'navigation_label' => 'Header Şablonları',
'model_label' => 'Header Şablonu',
'plural_model_label' => 'Header Şablonları',
// Sections
'section_general' => 'Genel Bilgiler',
// Form Fields
'field_title' => 'Başlık',
'field_html_content' => 'HTML İçerik',
'field_html_content_help' => 'Placeholder format: {tip.alan_adi} örn: {text.company_name}, {image.logo}, {email.contact}',
'field_is_active' => 'Aktif',
// Table Columns
'column_title' => 'Başlık',
'column_is_active' => 'Aktif',
'column_pages_count' => 'Sayfa Sayısı',
'column_created_at' => 'Oluşturulma',
'column_updated_at' => 'Güncellenme',
'column_deleted_at' => 'Silinme',
// Actions
'create' => 'Yeni Header Şablonu',
'edit' => 'Header Şablonunu Düzenle',
'view' => 'Header Şablonunu Görüntüle',
'delete' => 'Sil',
'restore' => 'Geri Yükle',
'force_delete' => 'Kalıcı Sil',
// Messages
'created_successfully' => 'Header şablonu başarıyla oluşturuldu.',
'updated_successfully' => 'Header şablonu başarıyla güncellendi.',
'deleted_successfully' => 'Header şablonu başarıyla silindi.',
'restored_successfully' => 'Header şablonu başarıyla geri yüklendi.',
];
+16
View File
@@ -147,4 +147,20 @@ return [
'value_type_datetime' => 'Tarih & Saat', 'value_type_datetime' => 'Tarih & Saat',
'value_type_array' => 'Dizi', 'value_type_array' => 'Dizi',
'value_type_json' => 'JSON', 'value_type_json' => 'JSON',
// Dynamic Template System
'form_section_header_template' => 'Header Şablonu',
'form_section_header_template_desc' => 'Sayfanın üst kısmı için dinamik header şablonu seçin',
'header_template_field' => 'Header Şablonu',
'form_section_template_sections' => 'Şablon Bölümleri',
'form_section_template_sections_desc' => 'Sayfa için dinamik şablon bölümleri ekleyin',
'template_sections_field' => 'Şablon Bölümleri',
'section_template_field' => 'Bölüm Şablonu',
'add_template_section' => 'Bölüm Ekle',
'template_section' => 'Şablon Bölümü',
'form_section_footer_template' => 'Footer Şablonu',
'form_section_footer_template_desc' => 'Sayfanın alt kısmı için dinamik footer şablonu seçin',
'footer_template_field' => 'Footer Şablonu',
]; ];
+40
View File
@@ -0,0 +1,40 @@
<?php
return [
// Navigation
'navigation_group' => 'Şablonlar',
'navigation_label' => 'Section Şablonları',
'model_label' => 'Section Şablonu',
'plural_model_label' => 'Section Şablonları',
// Sections
'section_general' => 'Genel Bilgiler',
// Form Fields
'field_title' => 'Başlık',
'field_html_content' => 'HTML İçerik',
'field_html_content_help' => 'Placeholder format: {tip.alan_adi} örn: {text.title}, {richtext.content}, {image.banner}',
'field_is_active' => 'Aktif',
// Table Columns
'column_title' => 'Başlık',
'column_is_active' => 'Aktif',
'column_created_at' => 'Oluşturulma',
'column_updated_at' => 'Güncellenme',
'column_deleted_at' => 'Silinme',
// Actions
'create' => 'Yeni Section Şablonu',
'edit' => 'Section Şablonunu Düzenle',
'view' => 'Section Şablonunu Görüntüle',
'delete' => 'Sil',
'restore' => 'Geri Yükle',
'force_delete' => 'Kalıcı Sil',
// Messages
'created_successfully' => 'Section şablonu başarıyla oluşturuldu.',
'updated_successfully' => 'Section şablonu başarıyla güncellendi.',
'deleted_successfully' => 'Section şablonu başarıyla silindi.',
'restored_successfully' => 'Section şablonu başarıyla geri yüklendi.',
];
@@ -0,0 +1,41 @@
<div class="page-wrapper">
{{-- Render Header Template --}}
@if($page->headerTemplate)
<div class="page-header">
{!! $renderHeader() !!}
</div>
@endif
{{-- Render Page Sections --}}
<main class="page-content">
{{-- Legacy sections (if exists) --}}
@if($page->sections && count($page->sections) > 0)
<div class="legacy-sections">
@foreach($page->sections as $section)
<x-dynamic-component :component="'blocks.' . ($section['type'] ?? 'custom')" :data="$section['data'] ?? []" />
@endforeach
</div>
@endif
{{-- New template-based sections --}}
@if($page->sections_data && count($page->sections_data) > 0)
<div class="template-sections">
{!! $renderSections() !!}
</div>
@endif
{{-- Fallback to content field if no sections --}}
@if((!$page->sections || count($page->sections) === 0) && (!$page->sections_data || count($page->sections_data) === 0))
<div class="page-content-body">
{!! $page->content !!}
</div>
@endif
</main>
{{-- Render Footer Template --}}
@if($page->footerTemplate)
<div class="page-footer">
{!! $renderFooter() !!}
</div>
@endif
</div>