From 875ea270cd5a66a53a802296f3d0e19600c4798f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Sat, 1 Nov 2025 17:39:08 -0300 Subject: [PATCH] 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. --- .../Resources/Pages/Schemas/PageForm.php | 918 +----------------- .../Pages/Schemas/Sections/ContentSection.php | 55 ++ .../Sections/FooterTemplateSection.php | 110 +++ .../Sections/HeaderTemplateSection.php | 110 +++ .../Schemas/Sections/PageSettingsSection.php | 98 ++ .../Sections/SectionBuilderSection.php | 376 +++++++ .../Sections/SectionTemplatesSection.php | 123 +++ .../Pages/Schemas/Sections/SeoSection.php | 33 + .../Schemas/Sections/TranslationsSection.php | 53 + 9 files changed, 993 insertions(+), 883 deletions(-) create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/ContentSection.php create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/FooterTemplateSection.php create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/HeaderTemplateSection.php create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/PageSettingsSection.php create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionBuilderSection.php create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionTemplatesSection.php create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/SeoSection.php create mode 100644 app/Filament/Admin/Resources/Pages/Schemas/Sections/TranslationsSection.php diff --git a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php index eb9424d..e21e21f 100644 --- a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php +++ b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php @@ -1,904 +1,56 @@ 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(), ]); } } diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/ContentSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/ContentSection.php new file mode 100644 index 0000000..81d0fd5 --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/ContentSection.php @@ -0,0 +1,55 @@ +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); + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/FooterTemplateSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/FooterTemplateSection.php new file mode 100644 index 0000000..150fb23 --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/FooterTemplateSection.php @@ -0,0 +1,110 @@ +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); + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/HeaderTemplateSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/HeaderTemplateSection.php new file mode 100644 index 0000000..4c2ec67 --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/HeaderTemplateSection.php @@ -0,0 +1,110 @@ +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); + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/PageSettingsSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/PageSettingsSection.php new file mode 100644 index 0000000..d6ccec4 --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/PageSettingsSection.php @@ -0,0 +1,98 @@ +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); + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionBuilderSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionBuilderSection.php new file mode 100644 index 0000000..c2fb31b --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionBuilderSection.php @@ -0,0 +1,376 @@ +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); + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionTemplatesSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionTemplatesSection.php new file mode 100644 index 0000000..ad41408 --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SectionTemplatesSection.php @@ -0,0 +1,123 @@ +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); + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/SeoSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SeoSection.php new file mode 100644 index 0000000..62123dd --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/SeoSection.php @@ -0,0 +1,33 @@ +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); + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Schemas/Sections/TranslationsSection.php b/app/Filament/Admin/Resources/Pages/Schemas/Sections/TranslationsSection.php new file mode 100644 index 0000000..a8d7328 --- /dev/null +++ b/app/Filament/Admin/Resources/Pages/Schemas/Sections/TranslationsSection.php @@ -0,0 +1,53 @@ +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); + } +} +