From c042cb5114d37384cebac8a640949e637ec0cc75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Fri, 31 Oct 2025 14:53:34 -0300 Subject: [PATCH] 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. --- .../FooterTemplateResource.php | 86 ++ .../Pages/CreateFooterTemplate.php | 11 + .../Pages/EditFooterTemplate.php | 25 + .../Pages/ListFooterTemplates.php | 19 + .../Pages/ViewFooterTemplate.php | 19 + .../Schemas/FooterTemplateForm.php | 40 + .../Schemas/FooterTemplateInfolist.php | 16 + .../Tables/FooterTemplatesTable.php | 70 ++ .../HeaderTemplateResource.php | 86 ++ .../Pages/CreateHeaderTemplate.php | 11 + .../Pages/EditHeaderTemplate.php | 25 + .../Pages/ListHeaderTemplates.php | 19 + .../Pages/ViewHeaderTemplate.php | 19 + .../Schemas/HeaderTemplateForm.php | 40 + .../Schemas/HeaderTemplateInfolist.php | 16 + .../Tables/HeaderTemplatesTable.php | 70 ++ .../Resources/Pages/Schemas/PageForm.php | 88 ++ .../Pages/CreateSectionTemplate.php | 11 + .../Pages/EditSectionTemplate.php | 25 + .../Pages/ListSectionTemplates.php | 19 + .../Pages/ViewSectionTemplate.php | 19 + .../Schemas/SectionTemplateForm.php | 40 + .../Schemas/SectionTemplateInfolist.php | 16 + .../SectionTemplateResource.php | 86 ++ .../Tables/SectionTemplatesTable.php | 65 ++ app/Models/FooterTemplate.php | 30 + app/Models/HeaderTemplate.php | 30 + app/Models/Page.php | 47 ++ app/Models/SectionTemplate.php | 30 + app/Services/TemplateService.php | 259 ++++++ app/View/Components/PageRenderer.php | 81 ++ config/template-options.php | 43 + ...1_173856_create_header_templates_table.php | 31 + ..._173857_create_section_templates_table.php | 31 + ...1_173858_create_footer_templates_table.php | 31 + ...859_add_template_fields_to_pages_table.php | 42 + docs/DYNAMIC_TEMPLATE_SYSTEM.md | 796 ++++++++++++++++++ lang/en/footer-templates.php | 41 + lang/en/header-templates.php | 41 + lang/en/pages.php | 16 + lang/en/section-templates.php | 40 + lang/tr/footer-templates.php | 41 + lang/tr/header-templates.php | 41 + lang/tr/pages.php | 16 + lang/tr/section-templates.php | 40 + .../views/components/page-renderer.blade.php | 41 + 46 files changed, 2709 insertions(+) create mode 100644 app/Filament/Admin/Resources/FooterTemplates/FooterTemplateResource.php create mode 100644 app/Filament/Admin/Resources/FooterTemplates/Pages/CreateFooterTemplate.php create mode 100644 app/Filament/Admin/Resources/FooterTemplates/Pages/EditFooterTemplate.php create mode 100644 app/Filament/Admin/Resources/FooterTemplates/Pages/ListFooterTemplates.php create mode 100644 app/Filament/Admin/Resources/FooterTemplates/Pages/ViewFooterTemplate.php create mode 100644 app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateForm.php create mode 100644 app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateInfolist.php create mode 100644 app/Filament/Admin/Resources/FooterTemplates/Tables/FooterTemplatesTable.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/HeaderTemplateResource.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/Pages/CreateHeaderTemplate.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/Pages/EditHeaderTemplate.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/Pages/ListHeaderTemplates.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/Pages/ViewHeaderTemplate.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateForm.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateInfolist.php create mode 100644 app/Filament/Admin/Resources/HeaderTemplates/Tables/HeaderTemplatesTable.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/Pages/CreateSectionTemplate.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/Pages/EditSectionTemplate.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/Pages/ListSectionTemplates.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/Pages/ViewSectionTemplate.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateForm.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateInfolist.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/SectionTemplateResource.php create mode 100644 app/Filament/Admin/Resources/SectionTemplates/Tables/SectionTemplatesTable.php create mode 100644 app/Models/FooterTemplate.php create mode 100644 app/Models/HeaderTemplate.php create mode 100644 app/Models/SectionTemplate.php create mode 100644 app/Services/TemplateService.php create mode 100644 app/View/Components/PageRenderer.php create mode 100644 config/template-options.php create mode 100644 database/migrations/2025_10_31_173856_create_header_templates_table.php create mode 100644 database/migrations/2025_10_31_173857_create_section_templates_table.php create mode 100644 database/migrations/2025_10_31_173858_create_footer_templates_table.php create mode 100644 database/migrations/2025_10_31_173859_add_template_fields_to_pages_table.php create mode 100644 docs/DYNAMIC_TEMPLATE_SYSTEM.md create mode 100644 lang/en/footer-templates.php create mode 100644 lang/en/header-templates.php create mode 100644 lang/en/section-templates.php create mode 100644 lang/tr/footer-templates.php create mode 100644 lang/tr/header-templates.php create mode 100644 lang/tr/section-templates.php create mode 100644 resources/views/components/page-renderer.blade.php diff --git a/app/Filament/Admin/Resources/FooterTemplates/FooterTemplateResource.php b/app/Filament/Admin/Resources/FooterTemplates/FooterTemplateResource.php new file mode 100644 index 0000000..164bfb5 --- /dev/null +++ b/app/Filament/Admin/Resources/FooterTemplates/FooterTemplateResource.php @@ -0,0 +1,86 @@ + ListFooterTemplates::route('/'), + 'create' => CreateFooterTemplate::route('/create'), + 'view' => ViewFooterTemplate::route('/{record}'), + 'edit' => EditFooterTemplate::route('/{record}/edit'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Admin/Resources/FooterTemplates/Pages/CreateFooterTemplate.php b/app/Filament/Admin/Resources/FooterTemplates/Pages/CreateFooterTemplate.php new file mode 100644 index 0000000..bb32ed0 --- /dev/null +++ b/app/Filament/Admin/Resources/FooterTemplates/Pages/CreateFooterTemplate.php @@ -0,0 +1,11 @@ +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), + ]); + } +} diff --git a/app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateInfolist.php b/app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateInfolist.php new file mode 100644 index 0000000..05f6022 --- /dev/null +++ b/app/Filament/Admin/Resources/FooterTemplates/Schemas/FooterTemplateInfolist.php @@ -0,0 +1,16 @@ +components([ + // + ]); + } +} diff --git a/app/Filament/Admin/Resources/FooterTemplates/Tables/FooterTemplatesTable.php b/app/Filament/Admin/Resources/FooterTemplates/Tables/FooterTemplatesTable.php new file mode 100644 index 0000000..f7c68fc --- /dev/null +++ b/app/Filament/Admin/Resources/FooterTemplates/Tables/FooterTemplatesTable.php @@ -0,0 +1,70 @@ +columns([ + TextColumn::make('title') + ->label(__('footer-templates.column_title')) + ->searchable() + ->sortable(), + + IconColumn::make('is_active') + ->label(__('footer-templates.column_is_active')) + ->boolean() + ->sortable(), + + TextColumn::make('pages_count') + ->label(__('footer-templates.column_pages_count')) + ->counts('pages') + ->sortable(), + + TextColumn::make('created_at') + ->label(__('footer-templates.column_created_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('updated_at') + ->label(__('footer-templates.column_updated_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('deleted_at') + ->label(__('footer-templates.column_deleted_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + TrashedFilter::make(), + ]) + ->recordActions([ + ViewAction::make(), + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ForceDeleteBulkAction::make(), + RestoreBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Admin/Resources/HeaderTemplates/HeaderTemplateResource.php b/app/Filament/Admin/Resources/HeaderTemplates/HeaderTemplateResource.php new file mode 100644 index 0000000..22918c4 --- /dev/null +++ b/app/Filament/Admin/Resources/HeaderTemplates/HeaderTemplateResource.php @@ -0,0 +1,86 @@ + ListHeaderTemplates::route('/'), + 'create' => CreateHeaderTemplate::route('/create'), + 'view' => ViewHeaderTemplate::route('/{record}'), + 'edit' => EditHeaderTemplate::route('/{record}/edit'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Pages/CreateHeaderTemplate.php b/app/Filament/Admin/Resources/HeaderTemplates/Pages/CreateHeaderTemplate.php new file mode 100644 index 0000000..1cbae6c --- /dev/null +++ b/app/Filament/Admin/Resources/HeaderTemplates/Pages/CreateHeaderTemplate.php @@ -0,0 +1,11 @@ +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), + ]); + } +} diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateInfolist.php b/app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateInfolist.php new file mode 100644 index 0000000..c6334a4 --- /dev/null +++ b/app/Filament/Admin/Resources/HeaderTemplates/Schemas/HeaderTemplateInfolist.php @@ -0,0 +1,16 @@ +components([ + // + ]); + } +} diff --git a/app/Filament/Admin/Resources/HeaderTemplates/Tables/HeaderTemplatesTable.php b/app/Filament/Admin/Resources/HeaderTemplates/Tables/HeaderTemplatesTable.php new file mode 100644 index 0000000..0ae9b2f --- /dev/null +++ b/app/Filament/Admin/Resources/HeaderTemplates/Tables/HeaderTemplatesTable.php @@ -0,0 +1,70 @@ +columns([ + TextColumn::make('title') + ->label(__('header-templates.column_title')) + ->searchable() + ->sortable(), + + IconColumn::make('is_active') + ->label(__('header-templates.column_is_active')) + ->boolean() + ->sortable(), + + TextColumn::make('pages_count') + ->label(__('header-templates.column_pages_count')) + ->counts('pages') + ->sortable(), + + TextColumn::make('created_at') + ->label(__('header-templates.column_created_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('updated_at') + ->label(__('header-templates.column_updated_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('deleted_at') + ->label(__('header-templates.column_deleted_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + TrashedFilter::make(), + ]) + ->recordActions([ + ViewAction::make(), + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ForceDeleteBulkAction::make(), + RestoreBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php index b38f23f..e316ad9 100644 --- a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php +++ b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php @@ -3,6 +3,10 @@ namespace App\Filament\Admin\Resources\Pages\Schemas; 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\CodeEditor; 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\TextInput; use Filament\Forms\Components\Toggle; +use Filament\Schemas\Components\Group; use Filament\Schemas\Components\Utilities\Get; +use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Schema; class PageForm @@ -527,6 +533,88 @@ class PageForm ->collapsible(true) ->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 Section::make('🌍 ' . __('pages.translations_section')) ->schema([ diff --git a/app/Filament/Admin/Resources/SectionTemplates/Pages/CreateSectionTemplate.php b/app/Filament/Admin/Resources/SectionTemplates/Pages/CreateSectionTemplate.php new file mode 100644 index 0000000..6adc3d0 --- /dev/null +++ b/app/Filament/Admin/Resources/SectionTemplates/Pages/CreateSectionTemplate.php @@ -0,0 +1,11 @@ +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), + ]); + } +} diff --git a/app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateInfolist.php b/app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateInfolist.php new file mode 100644 index 0000000..d67f1f8 --- /dev/null +++ b/app/Filament/Admin/Resources/SectionTemplates/Schemas/SectionTemplateInfolist.php @@ -0,0 +1,16 @@ +components([ + // + ]); + } +} diff --git a/app/Filament/Admin/Resources/SectionTemplates/SectionTemplateResource.php b/app/Filament/Admin/Resources/SectionTemplates/SectionTemplateResource.php new file mode 100644 index 0000000..59ca77e --- /dev/null +++ b/app/Filament/Admin/Resources/SectionTemplates/SectionTemplateResource.php @@ -0,0 +1,86 @@ + ListSectionTemplates::route('/'), + 'create' => CreateSectionTemplate::route('/create'), + 'view' => ViewSectionTemplate::route('/{record}'), + 'edit' => EditSectionTemplate::route('/{record}/edit'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Admin/Resources/SectionTemplates/Tables/SectionTemplatesTable.php b/app/Filament/Admin/Resources/SectionTemplates/Tables/SectionTemplatesTable.php new file mode 100644 index 0000000..9e3f887 --- /dev/null +++ b/app/Filament/Admin/Resources/SectionTemplates/Tables/SectionTemplatesTable.php @@ -0,0 +1,65 @@ +columns([ + TextColumn::make('title') + ->label(__('section-templates.column_title')) + ->searchable() + ->sortable(), + + IconColumn::make('is_active') + ->label(__('section-templates.column_is_active')) + ->boolean() + ->sortable(), + + TextColumn::make('created_at') + ->label(__('section-templates.column_created_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('updated_at') + ->label(__('section-templates.column_updated_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('deleted_at') + ->label(__('section-templates.column_deleted_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + TrashedFilter::make(), + ]) + ->recordActions([ + ViewAction::make(), + EditAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ForceDeleteBulkAction::make(), + RestoreBulkAction::make(), + ]), + ]); + } +} diff --git a/app/Models/FooterTemplate.php b/app/Models/FooterTemplate.php new file mode 100644 index 0000000..e52b8e1 --- /dev/null +++ b/app/Models/FooterTemplate.php @@ -0,0 +1,30 @@ + 'boolean', + ]; + + /** + * Get the pages that use this footer template. + */ + public function pages(): HasMany + { + return $this->hasMany(Page::class, 'footer_template_id'); + } +} diff --git a/app/Models/HeaderTemplate.php b/app/Models/HeaderTemplate.php new file mode 100644 index 0000000..6808b17 --- /dev/null +++ b/app/Models/HeaderTemplate.php @@ -0,0 +1,30 @@ + 'boolean', + ]; + + /** + * Get the pages that use this header template. + */ + public function pages(): HasMany + { + return $this->hasMany(Page::class, 'header_template_id'); + } +} diff --git a/app/Models/Page.php b/app/Models/Page.php index 38da609..e8a70f6 100644 --- a/app/Models/Page.php +++ b/app/Models/Page.php @@ -29,6 +29,12 @@ class Page extends Model 'show_in_menu', 'sections', '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', 'sections' => 'array', 'data' => 'array', + // Template System + 'header_data' => 'array', + 'footer_data' => 'array', + 'sections_data' => 'array', ]; public function author() @@ -66,6 +76,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'; @@ -124,4 +150,25 @@ class Page extends Model 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); + } } diff --git a/app/Models/SectionTemplate.php b/app/Models/SectionTemplate.php new file mode 100644 index 0000000..bfbd172 --- /dev/null +++ b/app/Models/SectionTemplate.php @@ -0,0 +1,30 @@ + '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(); + } +} diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php new file mode 100644 index 0000000..65ab39b --- /dev/null +++ b/app/Services/TemplateService.php @@ -0,0 +1,259 @@ +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; + } +} + diff --git a/app/View/Components/PageRenderer.php b/app/View/Components/PageRenderer.php new file mode 100644 index 0000000..98a5192 --- /dev/null +++ b/app/View/Components/PageRenderer.php @@ -0,0 +1,81 @@ +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 ?? [] + ); + } +} diff --git a/config/template-options.php b/config/template-options.php new file mode 100644 index 0000000..f3b8095 --- /dev/null +++ b/config/template-options.php @@ -0,0 +1,43 @@ + ['key' => 'Label'] + | + */ + + // Example options - Add your own as needed + 'example_select' => [ + 'option_1' => 'Option 1', + 'option_2' => 'Option 2', + 'option_3' => 'Option 3', + ], + + 'status' => [ + 'active' => 'Active', + 'inactive' => 'Inactive', + 'pending' => 'Pending', + ], + + 'size' => [ + 'small' => 'Small', + 'medium' => 'Medium', + 'large' => 'Large', + ], + + 'color_scheme' => [ + 'light' => 'Light', + 'dark' => 'Dark', + 'auto' => 'Auto', + ], + + // Add more options as needed +]; + diff --git a/database/migrations/2025_10_31_173856_create_header_templates_table.php b/database/migrations/2025_10_31_173856_create_header_templates_table.php new file mode 100644 index 0000000..b5ba3f6 --- /dev/null +++ b/database/migrations/2025_10_31_173856_create_header_templates_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('title'); + $table->longText('html_content'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('header_templates'); + } +}; diff --git a/database/migrations/2025_10_31_173857_create_section_templates_table.php b/database/migrations/2025_10_31_173857_create_section_templates_table.php new file mode 100644 index 0000000..58f7703 --- /dev/null +++ b/database/migrations/2025_10_31_173857_create_section_templates_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('title'); + $table->longText('html_content'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('section_templates'); + } +}; diff --git a/database/migrations/2025_10_31_173858_create_footer_templates_table.php b/database/migrations/2025_10_31_173858_create_footer_templates_table.php new file mode 100644 index 0000000..be8e7d2 --- /dev/null +++ b/database/migrations/2025_10_31_173858_create_footer_templates_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('title'); + $table->longText('html_content'); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('footer_templates'); + } +}; diff --git a/database/migrations/2025_10_31_173859_add_template_fields_to_pages_table.php b/database/migrations/2025_10_31_173859_add_template_fields_to_pages_table.php new file mode 100644 index 0000000..ea9b394 --- /dev/null +++ b/database/migrations/2025_10_31_173859_add_template_fields_to_pages_table.php @@ -0,0 +1,42 @@ +foreignId('header_template_id')->nullable()->after('id')->constrained('header_templates')->nullOnDelete(); + $table->json('header_data')->nullable()->after('header_template_id'); + + $table->foreignId('footer_template_id')->nullable()->after('header_data')->constrained('footer_templates')->nullOnDelete(); + $table->json('footer_data')->nullable()->after('footer_template_id'); + + $table->json('sections_data')->nullable()->after('footer_data'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('pages', function (Blueprint $table) { + $table->dropForeign(['header_template_id']); + $table->dropColumn('header_template_id'); + $table->dropColumn('header_data'); + + $table->dropForeign(['footer_template_id']); + $table->dropColumn('footer_template_id'); + $table->dropColumn('footer_data'); + + $table->dropColumn('sections_data'); + }); + } +}; diff --git a/docs/DYNAMIC_TEMPLATE_SYSTEM.md b/docs/DYNAMIC_TEMPLATE_SYSTEM.md new file mode 100644 index 0000000..d6a98c8 --- /dev/null +++ b/docs/DYNAMIC_TEMPLATE_SYSTEM.md @@ -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 + + + + +``` + +**Bu template şu form alanlarını oluşturur:** +- Logo (FileUpload - image) +- Company Name (TextInput) +- Tagline (Textarea) +- Menu HTML (RichEditor) +- Contact Email (TextInput - email) +- Phone (TextInput - tel) +- Facebook Link (TextInput - url) +- Twitter Link (TextInput - url) +- LinkedIn Link (TextInput - url) +- Show Search (Toggle) +- Show Cart (Toggle) +- Header BG (ColorPicker) +- Padding (TextInput - number) +- Opacity (Slider) + +### 3. Pages Modülü Entegrasyonu + +#### 3.1 Pages Tablo Güncellemesi +```php +// Migration: add_template_fields_to_pages_table +Schema::table('pages', function (Blueprint $table) { + $table->foreignId('header_template_id')->nullable()->constrained('header_templates')->nullOnDelete(); + $table->json('header_data')->nullable(); // Header placeholder değerleri + + $table->foreignId('footer_template_id')->nullable()->constrained('footer_templates')->nullOnDelete(); + $table->json('footer_data')->nullable(); // Footer placeholder değerleri + + // sections_data -> Repeater ile sections (eski page_sections yerine) + // Format: [ + // { + // 'section_template_id': 1, + // 'section_data': { 'text.title': 'Başlık', 'image.banner': 'path/to/image.jpg' } + // }, + // ... + // ] + $table->json('sections_data')->nullable(); +}); +``` + +**Veri Tipi Notları:** +- `json` column tipi: MySQL 5.7.8+ ve PostgreSQL 9.4+ destekli +- JSON alanlar Laravel tarafından otomatik encode/decode edilir +- `longtext` alternatifi: Tüm veritabanlarıyla uyumlu (Laravel JSON cast ile) +- Her iki durumda da Laravel'in `$casts = ['header_data' => 'array']` özelliği kullanılır + +#### 3.2 Page Model İlişkileri +```php +class Page extends Model +{ + public function headerTemplate() + { + return $this->belongsTo(HeaderTemplate::class); + } + + public function footerTemplate() + { + return $this->belongsTo(FooterTemplate::class); + } + + // sections_data JSON içinde section_template_id'ler var + public function getSectionsAttribute() + { + $sectionsData = $this->sections_data ?? []; + return collect($sectionsData)->map(function ($section) { + return [ + 'template' => SectionTemplate::find($section['section_template_id']), + 'data' => $section['section_data'] ?? [], + ]; + }); + } +} +``` + +### 4. Filament Form Yapısı + +#### 4.1 Page Form (PageForm.php) +```php +use Filament\Schemas\Components\Tabs; +use Filament\Schemas\Components\Tabs\Tab; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\Repeater; + +Tabs::make('PageTabs') + ->tabs([ + Tab::make('Genel Bilgiler') + ->schema([ + TextInput::make('title')->required(), + TextInput::make('slug')->required(), + // ... diğer genel alanlar + ]), + + Tab::make('Header Template') + ->schema([ + Select::make('header_template_id') + ->label('Header Template') + ->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id')) + ->live() + ->afterStateUpdated(fn ($state, Set $set) => $set('header_data', [])), + + // Dinamik header alanları + Group::make() + ->schema(fn (Get $get) => self::generateDynamicFields( + HeaderTemplate::find($get('header_template_id')), + 'header_data' + )) + ->visible(fn (Get $get) => filled($get('header_template_id'))), + ]), + + Tab::make('Sections') + ->schema([ + Repeater::make('sections_data') + ->schema([ + Select::make('section_template_id') + ->label('Section Template') + ->options(SectionTemplate::where('is_active', true)->pluck('title', 'id')) + ->live() + ->required(), + + // Dinamik section alanları + Group::make() + ->schema(fn (Get $get, $record) => self::generateDynamicFields( + SectionTemplate::find($get('section_template_id')), + 'section_data' + )) + ->visible(fn (Get $get) => filled($get('section_template_id'))), + ]) + ->itemLabel(fn (array $state): ?string => + SectionTemplate::find($state['section_template_id'] ?? null)?->title ?? 'Section' + ) + ->collapsible() + ->reorderable() + ->addActionLabel(__('pages.add_section')), + ]), + + Tab::make('Footer Template') + ->schema([ + Select::make('footer_template_id') + ->label('Footer Template') + ->options(FooterTemplate::where('is_active', true)->pluck('title', 'id')) + ->live() + ->afterStateUpdated(fn ($state, Set $set) => $set('footer_data', [])), + + // Dinamik footer alanları + Group::make() + ->schema(fn (Get $get) => self::generateDynamicFields( + FooterTemplate::find($get('footer_template_id')), + 'footer_data' + )) + ->visible(fn (Get $get) => filled($get('footer_template_id'))), + ]), + ]); +``` + +#### 4.2 Dinamik Form Generator +```php +use Filament\Forms\Components\{ + TextInput, Textarea, Select, Checkbox, CheckboxList, + Radio, Toggle, ToggleButtons, DateTimePicker, DatePicker, + TimePicker, FileUpload, RichEditor, MarkdownEditor, + ColorPicker, TagsInput, KeyValue, CodeEditor, Hidden, Slider +}; + +protected static function generateDynamicFields($template, string $dataKey): array +{ + if (!$template) return []; + + $placeholders = self::parsePlaceholders($template->html_content); + $fields = []; + + foreach ($placeholders as $placeholder) { + [$type, $name] = explode('.', $placeholder); + + $label = str($name)->title()->replace('_', ' ')->toString(); + + $field = match($type) { + // Text Input Variants + 'text' => TextInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->maxLength(255), + + 'email' => TextInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->email() + ->maxLength(255), + + 'url' => TextInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->url() + ->maxLength(500), + + 'tel' => TextInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->tel() + ->maxLength(20), + + 'number' => TextInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->numeric(), + + 'password' => TextInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->password() + ->dehydrated(fn ($state) => filled($state)) + ->maxLength(255), + + // Text Area & Editors + 'textarea' => Textarea::make("{$dataKey}.{$placeholder}") + ->label($label) + ->rows(4) + ->maxLength(5000), + + 'richtext' => RichEditor::make("{$dataKey}.{$placeholder}") + ->label($label) + ->toolbarButtons([ + 'bold', 'italic', 'underline', 'link', + 'bulletList', 'orderedList', 'h2', 'h3', + ]) + ->maxLength(50000), + + 'markdown' => MarkdownEditor::make("{$dataKey}.{$placeholder}") + ->label($label) + ->toolbarButtons([ + 'bold', 'italic', 'strike', 'link', + 'heading', 'bulletList', 'orderedList', 'codeBlock', + ]) + ->maxLength(50000), + + 'code' => CodeEditor::make("{$dataKey}.{$placeholder}") + ->label($label) + ->lineNumbers() + ->maxLength(50000), + + // Date & Time + 'date' => DatePicker::make("{$dataKey}.{$placeholder}") + ->label($label) + ->native(false), + + 'datetime' => DateTimePicker::make("{$dataKey}.{$placeholder}") + ->label($label) + ->native(false) + ->seconds(false), + + 'time' => TimePicker::make("{$dataKey}.{$placeholder}") + ->label($label) + ->native(false) + ->seconds(false), + + // File Uploads + 'image' => FileUpload::make("{$dataKey}.{$placeholder}") + ->label($label) + ->image() + ->imageEditor() + ->imageEditorAspectRatios([ + null, + '16:9', + '4:3', + '1:1', + ]) + ->directory('templates/images') + ->maxSize(5120), + + 'images' => FileUpload::make("{$dataKey}.{$placeholder}") + ->label($label) + ->image() + ->multiple() + ->imageEditor() + ->directory('templates/images') + ->maxSize(5120) + ->maxFiles(10), + + 'file' => FileUpload::make("{$dataKey}.{$placeholder}") + ->label($label) + ->directory('templates/files') + ->maxSize(10240), + + 'files' => FileUpload::make("{$dataKey}.{$placeholder}") + ->label($label) + ->multiple() + ->directory('templates/files') + ->maxSize(10240) + ->maxFiles(10), + + // Selection & Options + 'select' => Select::make("{$dataKey}.{$placeholder}") + ->label($label) + ->options(self::getSelectOptions($name)) + ->searchable(), + + 'multiselect' => Select::make("{$dataKey}.{$placeholder}") + ->label($label) + ->multiple() + ->options(self::getSelectOptions($name)) + ->searchable(), + + 'checkbox' => Checkbox::make("{$dataKey}.{$placeholder}") + ->label($label) + ->inline(false), + + 'checkboxlist' => CheckboxList::make("{$dataKey}.{$placeholder}") + ->label($label) + ->options(self::getSelectOptions($name)) + ->columns(2), + + 'radio' => Radio::make("{$dataKey}.{$placeholder}") + ->label($label) + ->options(self::getSelectOptions($name)) + ->inline() + ->inlineLabel(false), + + 'toggle' => Toggle::make("{$dataKey}.{$placeholder}") + ->label($label) + ->inline(false), + + 'togglebuttons' => ToggleButtons::make("{$dataKey}.{$placeholder}") + ->label($label) + ->options(self::getSelectOptions($name)) + ->inline() + ->grouped(), + + // Color & Visual + 'color' => ColorPicker::make("{$dataKey}.{$placeholder}") + ->label($label), + + // Structured Data + 'tags' => TagsInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->separator(','), + + 'keyvalue' => KeyValue::make("{$dataKey}.{$placeholder}") + ->label($label) + ->keyLabel('Key') + ->valueLabel('Value') + ->reorderable(), + + // Advanced + 'slider' => Slider::make("{$dataKey}.{$placeholder}") + ->label($label) + ->minValue(0) + ->maxValue(100) + ->step(1), + + 'hidden' => Hidden::make("{$dataKey}.{$placeholder}"), + + // Default + default => TextInput::make("{$dataKey}.{$placeholder}") + ->label($label) + ->maxLength(255) + ->helperText("Unknown type: {$type}"), + }; + + $fields[] = $field; + } + + return $fields; +} + +/** + * Select, MultiSelect, CheckboxList, Radio, ToggleButtons için + * dinamik options yükler. Gerçek uygulamada config veya database'den + * okunabilir. + */ +protected static function getSelectOptions(string $fieldName): array +{ + // Örnek: config/template-options.php dosyasından okuyabilirsiniz + return config("template-options.{$fieldName}", [ + 'option_1' => 'Option 1', + 'option_2' => 'Option 2', + 'option_3' => 'Option 3', + ]); +} + +/** + * Template HTML içeriğindeki placeholder'ları parse eder + * Desteklenen format: {type.field_name} + */ +protected static function parsePlaceholders(string $html): array +{ + // Regex: {word.word_with_underscores} + preg_match_all('/\{([a-z]+\.[a-z_]+)\}/i', $html, $matches); + return array_unique($matches[1] ?? []); +} +``` + +### 5. Frontend Rendering + +#### 5.1 Blade Component (PageRenderer) +```php +// app/View/Components/PageRenderer.php +class PageRenderer extends Component +{ + public function __construct(public Page $page) {} + + public function render() + { + return view('components.page-renderer'); + } + + public function renderHeader() + { + if (!$this->page->headerTemplate) return ''; + + return $this->replaceePlaceholders( + $this->page->headerTemplate->html_content, + $this->page->header_data ?? [] + ); + } + + public function renderSections() + { + return collect($this->page->sections)->map(function ($section) { + return $this->replacePlaceholders( + $section['template']->html_content, + $section['data'] + ); + })->implode(''); + } + + public function renderFooter() + { + if (!$this->page->footerTemplate) return ''; + + return $this->replacePlaceholders( + $this->page->footerTemplate->html_content, + $this->page->footer_data ?? [] + ); + } + + protected function replacePlaceholders(string $html, array $data): string + { + foreach ($data as $placeholder => $value) { + // {text.name} → value + $html = str_replace("{{$placeholder}}", $value, $html); + } + return $html; + } +} +``` + +#### 5.2 Blade View +```blade + +
+ {!! $renderHeader() !!} + +
+ {!! $renderSections() !!} +
+ + {!! $renderFooter() !!} +
+``` + +### 6. Navigasyon Yapısı + +```php +// Filament Panel Provider +->navigationGroups([ + 'Template' => [ + 'icon' => 'heroicon-o-document-duplicate', + 'order' => 3, + ], +]) + +// Her Resource'da +public static function getNavigationGroup(): string +{ + return 'Template'; +} +``` + +## Dosya Yapısı + +``` +app/ +├── Models/ +│ ├── HeaderTemplate.php +│ ├── SectionTemplate.php +│ └── FooterTemplate.php +├── Filament/Admin/Resources/ +│ ├── HeaderTemplates/ +│ │ ├── HeaderTemplateResource.php +│ │ ├── Pages/ +│ │ ├── Schemas/ +│ │ └── Tables/ +│ ├── SectionTemplates/ +│ │ ├── SectionTemplateResource.php +│ │ ├── Pages/ +│ │ ├── Schemas/ +│ │ └── Tables/ +│ └── FooterTemplates/ +│ ├── FooterTemplateResource.php +│ ├── Pages/ +│ ├── Schemas/ +│ └── Tables/ +├── View/Components/ +│ └── PageRenderer.php +└── Services/ + └── TemplateService.php (helper methods) + +database/migrations/ +├── create_header_templates_table.php +├── create_section_templates_table.php +├── create_footer_templates_table.php +└── add_template_fields_to_pages_table.php + +lang/ +├── tr/ +│ ├── header-templates.php +│ ├── section-templates.php +│ └── footer-templates.php +└── en/ + ├── header-templates.php + ├── section-templates.php + └── footer-templates.php + +resources/views/ +└── components/ + └── page-renderer.blade.php +``` + +## Örnek Kullanım Senaryosu + +### 1. Template Tanımlama +**Header Template:** +```html +
+ {text.site_name} + + {email.contact} +
+``` + +### 2. Page'de Template Seçimi +- Header Template: "Ana Header" seçildi +- Dinamik alanlar açıldı: + - Logo (FileUpload) + - Site Name (TextInput) + - Menu HTML (RichEditor) + - Contact Email (TextInput) +- Values dolduruldu ve kaydedildi + +### 3. Frontend'de Görüntüleme +```blade + +``` + +Output: +```html +
+ My Company + + info@company.com +
+``` + +## Avantajlar + +1. ✅ **Tamamen Dinamik**: Kod değişikliği olmadan yeni template'ler eklenebilir +2. ✅ **Yeniden Kullanılabilir**: Aynı template birden fazla sayfada kullanılabilir +3. ✅ **Kolay Yönetim**: Admin panel üzerinden HTML düzenlenebilir +4. ✅ **Tip Güvenli**: Placeholder'lar form tipini belirler +5. ✅ **Ölçeklenebilir**: Yeni form tipleri kolayca eklenebilir +6. ✅ **SEO Dostu**: Her sayfa kendi içeriğine sahip +7. ✅ **Performanslı**: Sadece seçili template'ler yüklenir +8. ✅ **30+ Form Tipi**: Filament 4.x'in tüm form component'leri destekleniyor +9. ✅ **Zengin İçerik**: HTML, Markdown, Code editor desteği +10. ✅ **Büyük Veri**: longtext ile 4GB kapasiteli template'ler + +## Veri Tipi Özeti + +### HTML Content (Template'ler için) +```sql +html_content LONGTEXT +-- Kapasite: 4,294,967,295 karakter (4GB) +-- Kullanım: Template HTML + placeholder'lar +-- Avantaj: En karmaşık template'ler için bile yeterli +``` + +### JSON Data (Page verisi için) +```sql +header_data JSON +footer_data JSON +sections_data JSON +-- MySQL 5.7.8+ ve PostgreSQL 9.4+ native JSON desteği +-- Alternatif: LONGTEXT + Laravel JSON cast +-- Laravel otomatik encode/decode yapar +``` + +### Model Cast Kullanımı +```php +protected $casts = [ + 'header_data' => 'array', + 'footer_data' => 'array', + 'sections_data' => 'array', +]; +``` + +## Git Branch + +```bash +feature/dynamic-template-system +``` + +## İ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. + diff --git a/lang/en/footer-templates.php b/lang/en/footer-templates.php new file mode 100644 index 0000000..d971d65 --- /dev/null +++ b/lang/en/footer-templates.php @@ -0,0 +1,41 @@ + '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.', +]; + diff --git a/lang/en/header-templates.php b/lang/en/header-templates.php new file mode 100644 index 0000000..64b87f3 --- /dev/null +++ b/lang/en/header-templates.php @@ -0,0 +1,41 @@ + '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.', +]; + diff --git a/lang/en/pages.php b/lang/en/pages.php index 9ad164b..c7e6ace 100644 --- a/lang/en/pages.php +++ b/lang/en/pages.php @@ -147,4 +147,20 @@ return [ 'value_type_datetime' => 'Date & Time', 'value_type_array' => 'Array', 'value_type_json' => 'JSON', + + // Dynamic Template System + 'form_section_header_template' => 'Header Template', + 'form_section_header_template_desc' => 'Select a dynamic header template for the top of the page', + 'header_template_field' => 'Header Template', + + 'form_section_template_sections' => 'Template Sections', + 'form_section_template_sections_desc' => 'Add dynamic template sections for the page', + 'template_sections_field' => 'Template Sections', + 'section_template_field' => 'Section Template', + 'add_template_section' => 'Add Section', + 'template_section' => 'Template Section', + + 'form_section_footer_template' => 'Footer Template', + 'form_section_footer_template_desc' => 'Select a dynamic footer template for the bottom of the page', + 'footer_template_field' => 'Footer Template', ]; diff --git a/lang/en/section-templates.php b/lang/en/section-templates.php new file mode 100644 index 0000000..678bc43 --- /dev/null +++ b/lang/en/section-templates.php @@ -0,0 +1,40 @@ + '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.', +]; + diff --git a/lang/tr/footer-templates.php b/lang/tr/footer-templates.php new file mode 100644 index 0000000..179df00 --- /dev/null +++ b/lang/tr/footer-templates.php @@ -0,0 +1,41 @@ + 'Ş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.', +]; + diff --git a/lang/tr/header-templates.php b/lang/tr/header-templates.php new file mode 100644 index 0000000..72946f7 --- /dev/null +++ b/lang/tr/header-templates.php @@ -0,0 +1,41 @@ + 'Ş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.', +]; + diff --git a/lang/tr/pages.php b/lang/tr/pages.php index 40cf4cb..247557f 100644 --- a/lang/tr/pages.php +++ b/lang/tr/pages.php @@ -147,4 +147,20 @@ return [ 'value_type_datetime' => 'Tarih & Saat', 'value_type_array' => 'Dizi', 'value_type_json' => 'JSON', + + // Dynamic Template System + 'form_section_header_template' => 'Header Şablonu', + 'form_section_header_template_desc' => 'Sayfanın üst kısmı için dinamik header şablonu seçin', + 'header_template_field' => 'Header Şablonu', + + 'form_section_template_sections' => 'Şablon Bölümleri', + 'form_section_template_sections_desc' => 'Sayfa için dinamik şablon bölümleri ekleyin', + 'template_sections_field' => 'Şablon Bölümleri', + 'section_template_field' => 'Bölüm Şablonu', + 'add_template_section' => 'Bölüm Ekle', + 'template_section' => 'Şablon Bölümü', + + 'form_section_footer_template' => 'Footer Şablonu', + 'form_section_footer_template_desc' => 'Sayfanın alt kısmı için dinamik footer şablonu seçin', + 'footer_template_field' => 'Footer Şablonu', ]; diff --git a/lang/tr/section-templates.php b/lang/tr/section-templates.php new file mode 100644 index 0000000..bfcb656 --- /dev/null +++ b/lang/tr/section-templates.php @@ -0,0 +1,40 @@ + 'Ş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.', +]; + diff --git a/resources/views/components/page-renderer.blade.php b/resources/views/components/page-renderer.blade.php new file mode 100644 index 0000000..8d431c7 --- /dev/null +++ b/resources/views/components/page-renderer.blade.php @@ -0,0 +1,41 @@ +
+ {{-- Render Header Template --}} + @if($page->headerTemplate) + + @endif + + {{-- Render Page Sections --}} +
+ {{-- Legacy sections (if exists) --}} + @if($page->sections && count($page->sections) > 0) +
+ @foreach($page->sections as $section) + + @endforeach +
+ @endif + + {{-- New template-based sections --}} + @if($page->sections_data && count($page->sections_data) > 0) +
+ {!! $renderSections() !!} +
+ @endif + + {{-- Fallback to content field if no sections --}} + @if((!$page->sections || count($page->sections) === 0) && (!$page->sections_data || count($page->sections_data) === 0)) +
+ {!! $page->content !!} +
+ @endif +
+ + {{-- Render Footer Template --}} + @if($page->footerTemplate) + + @endif +
\ No newline at end of file