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