Refactor PageForm schema to modular sections: Introduced dedicated classes for each section (Content, Page Settings, SEO, Section Builder, Header Template, Section Templates, Footer Template, Translations) to enhance maintainability and organization. Each section now encapsulates its own schema, improving clarity and separation of concerns in the form structure.

This commit is contained in:
Ümit Tunç
2025-11-01 17:39:08 -03:00
parent af0d7964ca
commit 875ea270cd
9 changed files with 993 additions and 883 deletions
@@ -1,904 +1,56 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\ContentSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\FooterTemplateSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\HeaderTemplateSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\PageSettingsSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionBuilderSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionTemplatesSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\TranslationsSection;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use 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;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\RichEditor;
use Filament\Schemas\Components\Section;
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
{
/**
* Configure the page form schema with modular sections.
*
* This form is organized into 8 sections for better maintainability:
* 1. Content - Basic page content (title, slug, content, excerpt)
* 2. Page Settings - Metadata and settings (image, author, status, etc.)
* 3. SEO - SEO metadata (meta title, meta description)
* 4. Section Builder - Dynamic page sections
* 5. Header Template - Dynamic header template system
* 6. Section Templates - Repeatable section templates
* 7. Footer Template - Dynamic footer template system
* 8. Translations - Multi-language support
*/
public static function configure(Schema $schema): Schema
{
return $schema
->columns(3)
->schema([
// Sol kolon - Ana içerik (2 sütun genişliğinde)
Section::make(__('pages.form_section_content'))
->schema([
TextInput::make('title')
->label(__('pages.title_field'))
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, $state, callable $set) {
if ($operation !== 'create') {
return;
}
$set('slug', \Str::slug($state));
}),
TextInput::make('slug')
->label(__('pages.slug_field'))
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->rules(['alpha_dash'])
->helperText(__('pages.slug_helper_text')),
RichEditor::make('content')
->label(__('pages.content_field'))
->required()
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('pages')
->fileAttachmentsVisibility('public')
->columnSpanFull(),
Textarea::make('excerpt')
->label(__('pages.excerpt_field'))
->rows(3)
->maxLength(500)
->helperText(__('pages.excerpt_helper_text'))
->columnSpanFull(),
])
->columnSpan(2)
->collapsible(false),
// Left Column - Main Content (2 columns width)
ContentSection::make(),
// Sağ kolon - Metadata ve öne çıkan görsel (1 sütun genişliğinde)
Section::make(__('pages.form_section_page_settings'))
->schema([
FileUpload::make('featured_image')
->label(__('pages.featured_image_field'))
->image()
->disk('public')
->directory('pages/featured')
->visibility('public')
->imageEditor()
->imageEditorAspectRatios([
'16:9',
'4:3',
'1:1',
])
->helperText(__('pages.featured_image_helper_text'))
->columnSpanFull(),
Select::make('author_id')
->label(__('pages.author_field'))
->relationship('author', 'name')
->default(auth()->id())
->required()
->columnSpanFull(),
DateTimePicker::make('published_at')
->label(__('pages.published_at_field'))
->displayFormat('d.m.Y H:i')
->helperText(__('pages.published_at_helper_text'))
->columnSpanFull(),
Select::make('status')
->label(__('pages.status_field'))
->options([
'draft' => __('pages.status_draft'),
'published' => __('pages.status_published'),
'archived' => __('pages.status_archived'),
])
->default('draft')
->required()
->columnSpanFull(),
Select::make('parent_id')
->label(__('pages.parent_field'))
->relationship('parent', 'title')
->searchable()
->preload()
->helperText(__('pages.parent_helper_text'))
->columnSpanFull(),
Select::make('template')
->label(__('pages.template_field'))
->options([
'default' => __('pages.template_default'),
'landing' => __('pages.template_landing'),
'blog' => __('pages.template_blog'),
'contact' => __('pages.template_contact'),
])
->default('default')
->columnSpanFull(),
TextInput::make('sort_order')
->label(__('pages.sort_order_field'))
->numeric()
->default(0)
->helperText(__('pages.sort_order_helper_text'))
->columnSpanFull(),
Checkbox::make('is_homepage')
->label(__('pages.is_homepage_field'))
->helperText(__('pages.is_homepage_helper_text'))
->columnSpanFull(),
Checkbox::make('show_in_menu')
->label(__('pages.show_in_menu_field'))
->default(true)
->helperText(__('pages.show_in_menu_helper_text'))
->columnSpanFull(),
])
->columnSpan(1)
->collapsible(false),
// Right Column - Settings and Metadata (1 column width)
PageSettingsSection::make(),
// Alt kısım - SEO ayarları (tam genişlik)
Section::make(__('pages.form_section_seo_settings'))
->schema([
TextInput::make('meta_title')
->label(__('pages.meta_title_field'))
->maxLength(60)
->helperText(__('pages.meta_title_helper_text')),
Textarea::make('meta_description')
->label(__('pages.meta_description_field'))
->rows(3)
->maxLength(160)
->helperText(__('pages.meta_description_helper_text'))
->columnSpanFull(),
])
->columns(2)
->columnSpanFull()
->collapsible(true)
->collapsed(true),
// Full Width - SEO Settings
SeoSection::make(),
// Sayfa Bölümleri (Section Builder)
Section::make(__('pages.form_section_sections'))
->schema([
Repeater::make('sections')
->label(__('pages.sections_field'))
->helperText(__('pages.sections_helper_text'))
->schema([
Select::make('type')
->label(__('pages.section_type'))
->helperText(__('pages.section_type_helper'))
->required()
->options([
'hero' => __('pages.section_type_hero'),
'features' => __('pages.section_type_features'),
'stats' => __('pages.section_type_stats'),
'cta' => __('pages.section_type_cta'),
'content' => __('pages.section_type_content'),
'gallery' => __('pages.section_type_gallery'),
'testimonials' => __('pages.section_type_testimonials'),
'team' => __('pages.section_type_team'),
'pricing' => __('pages.section_type_pricing'),
'faq' => __('pages.section_type_faq'),
'contact' => __('pages.section_type_contact'),
'custom' => __('pages.section_type_custom'),
])
->live()
->columnSpanFull(),
Repeater::make('data')
->label(__('pages.section_data'))
->helperText(__('pages.section_data_helper'))
->schema([
TextInput::make('key')
->label(__('pages.section_data_key'))
->helperText(__('pages.section_data_key_helper'))
->required()
->placeholder('title, subtitle, image, button_text, ...')
->columnSpan(1),
Select::make('type')
->label(__('pages.section_data_value_type'))
->required()
->options([
'text' => __('pages.value_type_text'),
'textarea' => __('pages.value_type_textarea'),
'html' => __('pages.value_type_html'),
'markdown' => __('pages.value_type_markdown'),
'richtext' => __('pages.value_type_richtext'),
'image' => __('pages.value_type_image'),
'file' => __('pages.value_type_file'),
'url' => __('pages.value_type_url'),
'email' => __('pages.value_type_email'),
'phone' => __('pages.value_type_phone'),
'number' => __('pages.value_type_number'),
'boolean' => __('pages.value_type_boolean'),
'color' => __('pages.value_type_color'),
'date' => __('pages.value_type_date'),
'datetime' => __('pages.value_type_datetime'),
'array' => __('pages.value_type_array'),
'json' => __('pages.value_type_json'),
])
->live()
->columnSpan(1),
// Single Hidden Field to store the actual value
Hidden::make('value'),
// Text Input (displays and updates 'value' field)
TextInput::make('_value_text')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => in_array($get('type'), ['text', 'url', 'email', 'phone', 'number']))
->numeric(fn (Get $get) => $get('type') === 'number')
->placeholder('Değer girin...')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
// Only hydrate if this field type is active
if (!in_array($get('type'), ['text', 'url', 'email', 'phone', 'number'])) {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Textarea (displays and updates 'value' field)
Textarea::make('_value_textarea')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'textarea')
->rows(4)
->placeholder('Metin girin...')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
// Only hydrate if this field type is active
if ($get('type') !== 'textarea') {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// HTML Code Editor (displays and updates 'value' field)
CodeEditor::make('_value_html')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'html')
->language(Language::Html)
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
// Only hydrate if this field type is html
if ($get('type') !== 'html') {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// JSON Code Editor (displays and updates 'value' field)
CodeEditor::make('_value_json')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'json')
->language(Language::Json)
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
// Only hydrate if this field type is json
if ($get('type') !== 'json') {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Rich Text Editor (displays and updates 'value' field)
RichEditor::make('_value_richtext')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'richtext')
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('pages/sections')
->toolbarButtons([
'bold',
'italic',
'link',
'bulletList',
'orderedList',
])
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
// Only hydrate if this field type is richtext
if ($get('type') !== 'richtext') {
return;
}
$value = $get('value');
// Skip if null or empty
if ($value === null || $value === '' || $value === []) {
return;
}
// Only set if it's valid TipTap JSON format
if (is_array($value) && isset($value['type']) && $value['type'] === 'doc') {
$component->state($value);
} elseif (is_string($value)) {
try {
$decoded = json_decode($value, true);
if (is_array($decoded) && isset($decoded['type']) && $decoded['type'] === 'doc') {
$component->state($decoded);
}
} catch (\Exception $e) {
// Invalid JSON, skip
}
}
})
->dehydrated(false)
->columnSpanFull(),
// Markdown Editor (displays and updates 'value' field)
MarkdownEditor::make('_value_markdown')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'markdown')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'markdown') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Image Upload (displays and updates 'value' field)
FileUpload::make('_value_image')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'image')
->image()
->disk('public')
->directory('pages/sections')
->imageEditor()
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'image') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// File Upload (displays and updates 'value' field)
FileUpload::make('_value_file')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'file')
->disk('public')
->directory('pages/sections')
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'file') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Boolean Toggle (displays and updates 'value' field)
Toggle::make('_value_boolean')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'boolean')
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'boolean') return;
$value = $get('value');
if ($value !== null && is_bool($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Color Picker (displays and updates 'value' field)
ColorPicker::make('_value_color')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'color')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'color') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Date Picker (displays and updates 'value' field)
DatePicker::make('_value_date')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'date')
->displayFormat('d/m/Y')
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'date') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// DateTime Picker (displays and updates 'value' field)
DateTimePicker::make('_value_datetime')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'datetime')
->displayFormat('d/m/Y H:i')
->seconds(false)
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'datetime') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Array Repeater (displays and updates 'value' field)
Repeater::make('_value_array')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'array')
->simple(
TextInput::make('item')
->label('Öğe')
->required()
)
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'array') return;
$value = $get('value');
if ($value !== null && is_array($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
])
->columns(2)
->defaultItems(0)
->addActionLabel(__('pages.section_data_add_field'))
->reorderable()
->collapsible()
->itemLabel(fn (array $state): ?string => isset($state['key']) ? $state['key'] . ' (' . ($state['type'] ?? 'text') . ')' : null)
->columnSpanFull(),
])
->defaultItems(0)
->addActionLabel(__('pages.section_add'))
->reorderable()
->collapsible()
->collapsed()
->itemLabel(fn (array $state): ?string => isset($state['type']) ? __('pages.section_type_' . $state['type']) : 'Bölüm')
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true),
// Full Width - Section Builder (Custom Page Sections)
SectionBuilderSection::make(),
// 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(function ($state, Set $set, Get $get) {
// Template seçildiğinde default verileri doldur
if (!$state) {
return;
}
$template = HeaderTemplate::find($state);
if (!$template) {
return;
}
// default_data'yı güvenli bir şekilde al
$defaultData = $template->default_data;
if (is_string($defaultData)) {
$defaultData = json_decode($defaultData, true) ?? [];
}
if (!is_array($defaultData) || empty($defaultData)) {
return;
}
// Mevcut header_data'yı al
$existingData = $get('header_data') ?? [];
// Nested array'leri dot notation'a çeviren recursive fonksiyon
$flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value)) {
$result = array_merge($result, $flattenDefaultData($value, $newKey));
} else {
$result[$newKey] = $value;
}
}
return $result;
};
// Default data'yı düzleştir
$flattenedDefaults = $flattenDefaultData($defaultData);
// Her default veri için boş olan alanları doldur
foreach ($flattenedDefaults as $key => $defaultValue) {
// Dot notation ile nested değeri kontrol et
$keys = explode('.', $key);
$currentValue = $existingData;
// Nested değere eriş
foreach ($keys as $k) {
$currentValue = $currentValue[$k] ?? null;
if ($currentValue === null) {
break;
}
}
// Eğer bu alan boşsa veya yoksa, default değeri kullan
if ($currentValue === null ||
$currentValue === '' ||
(is_array($currentValue) && empty($currentValue))) {
$set("header_data.{$key}", $defaultValue);
}
}
})
->columnSpanFull(),
Group::make()
->schema(function (Get $get, $record): array {
$templateId = $get('header_template_id');
if (!$templateId) {
return [];
}
$template = HeaderTemplate::find($templateId);
if (!$template) {
return [];
}
// Get existing page data
$existingData = $record?->header_data ?? [];
$templateDefaults = $template->default_data ?? [];
// Pass both existingData and defaultData separately
// TemplateService will handle empty value fallback
return TemplateService::generateDynamicFields(
$template,
'header_data',
$existingData,
$templateDefaults
);
})
->visible(fn (Get $get): bool => filled($get('header_template_id')))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true),
// Full Width - Dynamic Template System
HeaderTemplateSection::make(),
SectionTemplatesSection::make(),
FooterTemplateSection::make(),
Section::make(__('pages.form_section_template_sections'))
->description(__('pages.form_section_template_sections_desc'))
->schema([
Repeater::make('sections_data')
->label(__('pages.template_sections_field'))
->schema([
Select::make('section_template_id')
->label(__('pages.section_template_field'))
->options(SectionTemplate::where('is_active', true)->pluck('title', 'id'))
->searchable()
->live()
->required()
->afterStateUpdated(function ($state, Set $set, Get $get) {
// Template seçildiğinde default verileri doldur
if (!$state) {
return;
}
$template = SectionTemplate::find($state);
if (!$template) {
return;
}
// default_data'yı güvenli bir şekilde al
$defaultData = $template->default_data;
if (is_string($defaultData)) {
$defaultData = json_decode($defaultData, true) ?? [];
}
if (!is_array($defaultData) || empty($defaultData)) {
return;
}
// Mevcut section_data'yı al
$existingData = $get('section_data') ?? [];
// Nested array'leri dot notation'a çeviren recursive fonksiyon
$flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value)) {
$result = array_merge($result, $flattenDefaultData($value, $newKey));
} else {
$result[$newKey] = $value;
}
}
return $result;
};
// Default data'yı düzleştir
$flattenedDefaults = $flattenDefaultData($defaultData);
// Her default veri için boş olan alanları doldur
foreach ($flattenedDefaults as $key => $defaultValue) {
// Dot notation ile nested değeri kontrol et
$keys = explode('.', $key);
$currentValue = $existingData;
// Nested değere eriş
foreach ($keys as $k) {
$currentValue = $currentValue[$k] ?? null;
if ($currentValue === null) {
break;
}
}
// Eğer bu alan boşsa veya yoksa, default değeri kullan
if ($currentValue === null ||
$currentValue === '' ||
(is_array($currentValue) && empty($currentValue))) {
$set("section_data.{$key}", $defaultValue);
}
}
})
->columnSpanFull(),
Group::make()
->schema(function (Get $get, $state): array {
$templateId = $get('section_template_id');
if (!$templateId) {
return [];
}
$template = SectionTemplate::find($templateId);
if (!$template) {
return [];
}
// Get existing section data
$existingData = $get('section_data') ?? [];
$templateDefaults = $template->default_data ?? [];
// Pass both existingData and defaultData separately
// TemplateService will handle empty value fallback
return TemplateService::generateDynamicFields(
$template,
'section_data',
$existingData,
$templateDefaults
);
})
->visible(fn (Get $get): bool => filled($get('section_template_id')))
->columnSpanFull(),
])
->defaultItems(0)
->addActionLabel(__('pages.add_template_section'))
->reorderable()
->collapsible()
->collapsed()
->itemLabel(__('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(function ($state, Set $set, Get $get) {
// Template seçildiğinde default verileri doldur
if (!$state) {
return;
}
$template = FooterTemplate::find($state);
if (!$template) {
return;
}
// default_data'yı güvenli bir şekilde al
$defaultData = $template->default_data;
if (is_string($defaultData)) {
$defaultData = json_decode($defaultData, true) ?? [];
}
if (!is_array($defaultData) || empty($defaultData)) {
return;
}
// Mevcut footer_data'yı al
$existingData = $get('footer_data') ?? [];
// Nested array'leri dot notation'a çeviren recursive fonksiyon
$flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value)) {
$result = array_merge($result, $flattenDefaultData($value, $newKey));
} else {
$result[$newKey] = $value;
}
}
return $result;
};
// Default data'yı düzleştir
$flattenedDefaults = $flattenDefaultData($defaultData);
// Her default veri için boş olan alanları doldur
foreach ($flattenedDefaults as $key => $defaultValue) {
// Dot notation ile nested değeri kontrol et
$keys = explode('.', $key);
$currentValue = $existingData;
// Nested değere eriş
foreach ($keys as $k) {
$currentValue = $currentValue[$k] ?? null;
if ($currentValue === null) {
break;
}
}
// Eğer bu alan boşsa veya yoksa, default değeri kullan
if ($currentValue === null ||
$currentValue === '' ||
(is_array($currentValue) && empty($currentValue))) {
$set("footer_data.{$key}", $defaultValue);
}
}
})
->columnSpanFull(),
Group::make()
->schema(function (Get $get, $record): array {
$templateId = $get('footer_template_id');
if (!$templateId) {
return [];
}
$template = FooterTemplate::find($templateId);
if (!$template) {
return [];
}
// Get existing page data
$existingData = $record?->footer_data ?? [];
$templateDefaults = $template->default_data ?? [];
// Pass both existingData and defaultData separately
// TemplateService will handle empty value fallback
return TemplateService::generateDynamicFields(
$template,
'footer_data',
$existingData,
$templateDefaults
);
})
->visible(fn (Get $get): bool => filled($get('footer_template_id')))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true),
// Çeviri Sekmesi
Section::make('🌍 ' . __('pages.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('pages.title_field'),
'required' => false,
'maxLength' => 255,
],
'content' => [
'type' => 'richtext',
'label' => __('pages.content_field'),
'required' => false,
],
'excerpt' => [
'type' => 'textarea',
'label' => __('pages.excerpt_field'),
'required' => false,
'maxLength' => 500,
'rows' => 3,
],
'meta_title' => [
'type' => 'text',
'label' => __('pages.meta_title_field'),
'required' => false,
'maxLength' => 60,
],
'meta_description' => [
'type' => 'textarea',
'label' => __('pages.meta_description_field'),
'required' => false,
'maxLength' => 160,
'rows' => 3,
],
]),
])
->columnSpanFull()
->collapsible(true)
->collapsed(false),
// Full Width - Translations
TranslationsSection::make(),
]);
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
class ContentSection
{
public static function make(): Section
{
return Section::make(__('pages.form_section_content'))
->schema([
TextInput::make('title')
->label(__('pages.title_field'))
->required()
->maxLength(255)
->live(onBlur: true)
->afterStateUpdated(function (string $operation, $state, callable $set) {
if ($operation !== 'create') {
return;
}
$set('slug', \Str::slug($state));
}),
TextInput::make('slug')
->label(__('pages.slug_field'))
->required()
->maxLength(255)
->unique(ignoreRecord: true)
->rules(['alpha_dash'])
->helperText(__('pages.slug_helper_text')),
RichEditor::make('content')
->label(__('pages.content_field'))
->required()
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('pages')
->fileAttachmentsVisibility('public')
->columnSpanFull(),
Textarea::make('excerpt')
->label(__('pages.excerpt_field'))
->rows(3)
->maxLength(500)
->helperText(__('pages.excerpt_helper_text'))
->columnSpanFull(),
])
->columnSpan(2)
->collapsible(false);
}
}
@@ -0,0 +1,110 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use App\Models\FooterTemplate;
use App\Services\TemplateService;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
class FooterTemplateSection
{
public static function make(): Section
{
return Section::make(__('pages.form_section_footer_template'))
->description(__('pages.form_section_footer_template_desc'))
->schema([
Select::make('footer_template_id')
->label(__('pages.footer_template_field'))
->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
->searchable()
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
if (!$state) {
return;
}
$template = FooterTemplate::find($state);
if (!$template) {
return;
}
$defaultData = $template->default_data;
if (is_string($defaultData)) {
$defaultData = json_decode($defaultData, true) ?? [];
}
if (!is_array($defaultData) || empty($defaultData)) {
return;
}
$existingData = $get('footer_data') ?? [];
$flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value)) {
$result = array_merge($result, $flattenDefaultData($value, $newKey));
} else {
$result[$newKey] = $value;
}
}
return $result;
};
$flattenedDefaults = $flattenDefaultData($defaultData);
foreach ($flattenedDefaults as $key => $defaultValue) {
$keys = explode('.', $key);
$currentValue = $existingData;
foreach ($keys as $k) {
$currentValue = $currentValue[$k] ?? null;
if ($currentValue === null) {
break;
}
}
if ($currentValue === null ||
$currentValue === '' ||
(is_array($currentValue) && empty($currentValue))) {
$set("footer_data.{$key}", $defaultValue);
}
}
})
->columnSpanFull(),
Group::make()
->schema(function (Get $get, $record): array {
$templateId = $get('footer_template_id');
if (!$templateId) {
return [];
}
$template = FooterTemplate::find($templateId);
if (!$template) {
return [];
}
$existingData = $record?->footer_data ?? [];
$templateDefaults = $template->default_data ?? [];
return TemplateService::generateDynamicFields(
$template,
'footer_data',
$existingData,
$templateDefaults
);
})
->visible(fn (Get $get): bool => filled($get('footer_template_id')))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true);
}
}
@@ -0,0 +1,110 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use App\Models\HeaderTemplate;
use App\Services\TemplateService;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
class HeaderTemplateSection
{
public static function make(): Section
{
return Section::make(__('pages.form_section_header_template'))
->description(__('pages.form_section_header_template_desc'))
->schema([
Select::make('header_template_id')
->label(__('pages.header_template_field'))
->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
->searchable()
->live()
->afterStateUpdated(function ($state, Set $set, Get $get) {
if (!$state) {
return;
}
$template = HeaderTemplate::find($state);
if (!$template) {
return;
}
$defaultData = $template->default_data;
if (is_string($defaultData)) {
$defaultData = json_decode($defaultData, true) ?? [];
}
if (!is_array($defaultData) || empty($defaultData)) {
return;
}
$existingData = $get('header_data') ?? [];
$flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value)) {
$result = array_merge($result, $flattenDefaultData($value, $newKey));
} else {
$result[$newKey] = $value;
}
}
return $result;
};
$flattenedDefaults = $flattenDefaultData($defaultData);
foreach ($flattenedDefaults as $key => $defaultValue) {
$keys = explode('.', $key);
$currentValue = $existingData;
foreach ($keys as $k) {
$currentValue = $currentValue[$k] ?? null;
if ($currentValue === null) {
break;
}
}
if ($currentValue === null ||
$currentValue === '' ||
(is_array($currentValue) && empty($currentValue))) {
$set("header_data.{$key}", $defaultValue);
}
}
})
->columnSpanFull(),
Group::make()
->schema(function (Get $get, $record): array {
$templateId = $get('header_template_id');
if (!$templateId) {
return [];
}
$template = HeaderTemplate::find($templateId);
if (!$template) {
return [];
}
$existingData = $record?->header_data ?? [];
$templateDefaults = $template->default_data ?? [];
return TemplateService::generateDynamicFields(
$template,
'header_data',
$existingData,
$templateDefaults
);
})
->visible(fn (Get $get): bool => filled($get('header_template_id')))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true);
}
}
@@ -0,0 +1,98 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
class PageSettingsSection
{
public static function make(): Section
{
return Section::make(__('pages.form_section_page_settings'))
->schema([
FileUpload::make('featured_image')
->label(__('pages.featured_image_field'))
->image()
->disk('public')
->directory('pages/featured')
->visibility('public')
->imageEditor()
->imageEditorAspectRatios([
'16:9',
'4:3',
'1:1',
])
->helperText(__('pages.featured_image_helper_text'))
->columnSpanFull(),
Select::make('author_id')
->label(__('pages.author_field'))
->relationship('author', 'name')
->default(auth()->id())
->required()
->columnSpanFull(),
DateTimePicker::make('published_at')
->label(__('pages.published_at_field'))
->displayFormat('d.m.Y H:i')
->helperText(__('pages.published_at_helper_text'))
->columnSpanFull(),
Select::make('status')
->label(__('pages.status_field'))
->options([
'draft' => __('pages.status_draft'),
'published' => __('pages.status_published'),
'archived' => __('pages.status_archived'),
])
->default('draft')
->required()
->columnSpanFull(),
Select::make('parent_id')
->label(__('pages.parent_field'))
->relationship('parent', 'title')
->searchable()
->preload()
->helperText(__('pages.parent_helper_text'))
->columnSpanFull(),
Select::make('template')
->label(__('pages.template_field'))
->options([
'default' => __('pages.template_default'),
'landing' => __('pages.template_landing'),
'blog' => __('pages.template_blog'),
'contact' => __('pages.template_contact'),
])
->default('default')
->columnSpanFull(),
TextInput::make('sort_order')
->label(__('pages.sort_order_field'))
->numeric()
->default(0)
->helperText(__('pages.sort_order_helper_text'))
->columnSpanFull(),
Checkbox::make('is_homepage')
->label(__('pages.is_homepage_field'))
->helperText(__('pages.is_homepage_helper_text'))
->columnSpanFull(),
Checkbox::make('show_in_menu')
->label(__('pages.show_in_menu_field'))
->default(true)
->helperText(__('pages.show_in_menu_helper_text'))
->columnSpanFull(),
])
->columnSpan(1)
->collapsible(false);
}
}
@@ -0,0 +1,376 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\CodeEditor;
use Filament\Forms\Components\CodeEditor\Enums\Language;
use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class SectionBuilderSection
{
public static function make(): Section
{
return Section::make(__('pages.form_section_sections'))
->schema([
Repeater::make('sections')
->label(__('pages.sections_field'))
->helperText(__('pages.sections_helper_text'))
->schema([
Select::make('type')
->label(__('pages.section_type'))
->helperText(__('pages.section_type_helper'))
->required()
->options([
'hero' => __('pages.section_type_hero'),
'features' => __('pages.section_type_features'),
'stats' => __('pages.section_type_stats'),
'cta' => __('pages.section_type_cta'),
'content' => __('pages.section_type_content'),
'gallery' => __('pages.section_type_gallery'),
'testimonials' => __('pages.section_type_testimonials'),
'team' => __('pages.section_type_team'),
'pricing' => __('pages.section_type_pricing'),
'faq' => __('pages.section_type_faq'),
'contact' => __('pages.section_type_contact'),
'custom' => __('pages.section_type_custom'),
])
->live()
->columnSpanFull(),
Repeater::make('data')
->label(__('pages.section_data'))
->helperText(__('pages.section_data_helper'))
->schema([
TextInput::make('key')
->label(__('pages.section_data_key'))
->helperText(__('pages.section_data_key_helper'))
->required()
->placeholder('title, subtitle, image, button_text, ...')
->columnSpan(1),
Select::make('type')
->label(__('pages.section_data_value_type'))
->required()
->options([
'text' => __('pages.value_type_text'),
'textarea' => __('pages.value_type_textarea'),
'html' => __('pages.value_type_html'),
'markdown' => __('pages.value_type_markdown'),
'richtext' => __('pages.value_type_richtext'),
'image' => __('pages.value_type_image'),
'file' => __('pages.value_type_file'),
'url' => __('pages.value_type_url'),
'email' => __('pages.value_type_email'),
'phone' => __('pages.value_type_phone'),
'number' => __('pages.value_type_number'),
'boolean' => __('pages.value_type_boolean'),
'color' => __('pages.value_type_color'),
'date' => __('pages.value_type_date'),
'datetime' => __('pages.value_type_datetime'),
'array' => __('pages.value_type_array'),
'json' => __('pages.value_type_json'),
])
->live()
->columnSpan(1),
// Single Hidden Field to store the actual value
Hidden::make('value'),
// Text Input
TextInput::make('_value_text')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => in_array($get('type'), ['text', 'url', 'email', 'phone', 'number']))
->numeric(fn (Get $get) => $get('type') === 'number')
->placeholder('Değer girin...')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if (!in_array($get('type'), ['text', 'url', 'email', 'phone', 'number'])) {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Textarea
Textarea::make('_value_textarea')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'textarea')
->rows(4)
->placeholder('Metin girin...')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'textarea') {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// HTML Code Editor
CodeEditor::make('_value_html')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'html')
->language(Language::Html)
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'html') {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// JSON Code Editor
CodeEditor::make('_value_json')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'json')
->language(Language::Json)
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'json') {
return;
}
$value = $get('value');
if ($value !== null) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Rich Text Editor
RichEditor::make('_value_richtext')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'richtext')
->fileAttachmentsDisk('public')
->fileAttachmentsDirectory('pages/sections')
->toolbarButtons([
'bold',
'italic',
'link',
'bulletList',
'orderedList',
])
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'richtext') {
return;
}
$value = $get('value');
if ($value === null || $value === '' || $value === []) {
return;
}
if (is_array($value) && isset($value['type']) && $value['type'] === 'doc') {
$component->state($value);
} elseif (is_string($value)) {
try {
$decoded = json_decode($value, true);
if (is_array($decoded) && isset($decoded['type']) && $decoded['type'] === 'doc') {
$component->state($decoded);
}
} catch (\Exception $e) {
// Invalid JSON, skip
}
}
})
->dehydrated(false)
->columnSpanFull(),
// Markdown Editor
MarkdownEditor::make('_value_markdown')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'markdown')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'markdown') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Image Upload
FileUpload::make('_value_image')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'image')
->image()
->disk('public')
->directory('pages/sections')
->imageEditor()
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'image') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// File Upload
FileUpload::make('_value_file')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'file')
->disk('public')
->directory('pages/sections')
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'file') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Boolean Toggle
Toggle::make('_value_boolean')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'boolean')
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'boolean') return;
$value = $get('value');
if ($value !== null && is_bool($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Color Picker
ColorPicker::make('_value_color')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'color')
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'color') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Date Picker
DatePicker::make('_value_date')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'date')
->displayFormat('d/m/Y')
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'date') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// DateTime Picker
DateTimePicker::make('_value_datetime')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'datetime')
->displayFormat('d/m/Y H:i')
->seconds(false)
->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'datetime') return;
$value = $get('value');
if ($value !== null && is_string($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
// Array Repeater
Repeater::make('_value_array')
->label(__('pages.section_data_value'))
->visible(fn (Get $get) => $get('type') === 'array')
->simple(
TextInput::make('item')
->label('Öğe')
->required()
)
->live(onBlur: true)
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) {
if ($get('type') !== 'array') return;
$value = $get('value');
if ($value !== null && is_array($value)) {
$component->state($value);
}
})
->dehydrated(false)
->columnSpanFull(),
])
->columns(2)
->defaultItems(0)
->addActionLabel(__('pages.section_data_add_field'))
->reorderable()
->collapsible()
->itemLabel(fn (array $state): ?string => isset($state['key']) ? $state['key'] . ' (' . ($state['type'] ?? 'text') . ')' : null)
->columnSpanFull(),
])
->defaultItems(0)
->addActionLabel(__('pages.section_add'))
->reorderable()
->collapsible()
->collapsed()
->itemLabel(fn (array $state): ?string => isset($state['type']) ? __('pages.section_type_' . $state['type']) : 'Bölüm')
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true);
}
}
@@ -0,0 +1,123 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use App\Models\SectionTemplate;
use App\Services\TemplateService;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
class SectionTemplatesSection
{
public static function make(): Section
{
return Section::make(__('pages.form_section_template_sections'))
->description(__('pages.form_section_template_sections_desc'))
->schema([
Repeater::make('sections_data')
->label(__('pages.template_sections_field'))
->schema([
Select::make('section_template_id')
->label(__('pages.section_template_field'))
->options(SectionTemplate::where('is_active', true)->pluck('title', 'id'))
->searchable()
->live()
->required()
->afterStateUpdated(function ($state, Set $set, Get $get) {
if (!$state) {
return;
}
$template = SectionTemplate::find($state);
if (!$template) {
return;
}
$defaultData = $template->default_data;
if (is_string($defaultData)) {
$defaultData = json_decode($defaultData, true) ?? [];
}
if (!is_array($defaultData) || empty($defaultData)) {
return;
}
$existingData = $get('section_data') ?? [];
$flattenDefaultData = function ($array, $prefix = '') use (&$flattenDefaultData) {
$result = [];
foreach ($array as $key => $value) {
$newKey = $prefix ? "{$prefix}.{$key}" : $key;
if (is_array($value)) {
$result = array_merge($result, $flattenDefaultData($value, $newKey));
} else {
$result[$newKey] = $value;
}
}
return $result;
};
$flattenedDefaults = $flattenDefaultData($defaultData);
foreach ($flattenedDefaults as $key => $defaultValue) {
$keys = explode('.', $key);
$currentValue = $existingData;
foreach ($keys as $k) {
$currentValue = $currentValue[$k] ?? null;
if ($currentValue === null) {
break;
}
}
if ($currentValue === null ||
$currentValue === '' ||
(is_array($currentValue) && empty($currentValue))) {
$set("section_data.{$key}", $defaultValue);
}
}
})
->columnSpanFull(),
Group::make()
->schema(function (Get $get, $state): array {
$templateId = $get('section_template_id');
if (!$templateId) {
return [];
}
$template = SectionTemplate::find($templateId);
if (!$template) {
return [];
}
$existingData = $get('section_data') ?? [];
$templateDefaults = $template->default_data ?? [];
return TemplateService::generateDynamicFields(
$template,
'section_data',
$existingData,
$templateDefaults
);
})
->visible(fn (Get $get): bool => filled($get('section_template_id')))
->columnSpanFull(),
])
->defaultItems(0)
->addActionLabel(__('pages.add_template_section'))
->reorderable()
->collapsible()
->collapsed()
->itemLabel(__('pages.template_section'))
->columnSpanFull(),
])
->columnSpanFull()
->collapsible(true)
->collapsed(true);
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
class SeoSection
{
public static function make(): Section
{
return Section::make(__('pages.form_section_seo_settings'))
->schema([
TextInput::make('meta_title')
->label(__('pages.meta_title_field'))
->maxLength(60)
->helperText(__('pages.meta_title_helper_text')),
Textarea::make('meta_description')
->label(__('pages.meta_description_field'))
->rows(3)
->maxLength(160)
->helperText(__('pages.meta_description_helper_text'))
->columnSpanFull(),
])
->columns(2)
->columnSpanFull()
->collapsible(true)
->collapsed(true);
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use Filament\Schemas\Components\Section;
class TranslationsSection
{
public static function make(): Section
{
return Section::make('🌍 ' . __('pages.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('pages.title_field'),
'required' => false,
'maxLength' => 255,
],
'content' => [
'type' => 'richtext',
'label' => __('pages.content_field'),
'required' => false,
],
'excerpt' => [
'type' => 'textarea',
'label' => __('pages.excerpt_field'),
'required' => false,
'maxLength' => 500,
'rows' => 3,
],
'meta_title' => [
'type' => 'text',
'label' => __('pages.meta_title_field'),
'required' => false,
'maxLength' => 60,
],
'meta_description' => [
'type' => 'textarea',
'label' => __('pages.meta_description_field'),
'required' => false,
'maxLength' => 160,
'rows' => 3,
],
]),
])
->columnSpanFull()
->collapsible(true)
->collapsed(false);
}
}