diff --git a/.cursorrules b/.cursorrules index 948defd..2c586b7 100644 --- a/.cursorrules +++ b/.cursorrules @@ -101,8 +101,12 @@ use Filament\Forms\Components\FileUpload; // Layout components (ÖNEMLİ: Schemas namespace!) use Filament\Schemas\Components\Section; // ✅ Doğru use Filament\Schemas\Components\Grid; // ✅ Doğru -use Filament\Schemas\Components\Tabs; // ✅ Doğru use Filament\Schemas\Components\Fieldset; // ✅ Doğru + +// Tabs components (ÖNEMLİ: Forms namespace!) +use Filament\Forms\Components\Tabs; // ✅ Doğru +// Tab kullanımı: Tabs\Tab::make() şeklinde kullan +// Import gerekmez, Tabs namespace'i üzerinden erişilir ``` #### **YANLIŞ KULLANIM (Eski Filament 3.x):** @@ -127,7 +131,6 @@ $table - ✅ **Table Methods:** `->recordActions()` ve `->toolbarActions()` ### Dil Dosyası Yapısı -```php 'Bu alan zorunludur.', 'field_unique' => 'Bu değer zaten kullanılıyor.', ]; -``` + ### Model Kuralları - `$fillable` array'ini tanımla diff --git a/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php index fbd5dad..b0c3f01 100644 --- a/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php +++ b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php @@ -3,6 +3,7 @@ namespace App\Filament\Admin\Resources\Blogs\Pages; use App\Filament\Admin\Resources\Blogs\BlogResource; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Resources\Pages\CreateRecord; class CreateBlog extends CreateRecord @@ -23,4 +24,10 @@ class CreateBlog extends CreateRecord { return __('blog.created_successfully'); } + + protected function afterCreate(): void + { + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php index af1a630..495d912 100644 --- a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php +++ b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php @@ -3,6 +3,7 @@ namespace App\Filament\Admin\Resources\Blogs\Pages; use App\Filament\Admin\Resources\Blogs\BlogResource; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Actions\DeleteAction; use Filament\Actions\ForceDeleteAction; use Filament\Actions\RestoreAction; @@ -38,4 +39,28 @@ class EditBlog extends EditRecord { return __('blog.updated_successfully'); } + + protected function mutateFormDataBeforeFill(array $data): array + { + // Load existing translations + $translationData = TranslationTabs::fillFromRecord($this->record); + + \Log::info('Loading translations for edit', [ + 'blog_id' => $this->record->id, + 'translations' => $translationData + ]); + + return array_merge($data, $translationData); + } + + protected function afterSave(): void + { + \Log::info('Saving blog translations', [ + 'blog_id' => $this->record->id, + 'form_state' => $this->form->getState() + ]); + + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php index 13c0a6c..15347c5 100644 --- a/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php +++ b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Blogs\Schemas; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\FileUpload; @@ -149,6 +150,47 @@ class BlogForm ->columnSpanFull() ->collapsible(true) ->collapsed(true), + + // Çeviri Sekmesi + Section::make('🌍 ' . __('blog.translations_section')) + ->schema([ + TranslationTabs::make([ + 'title' => [ + 'type' => 'text', + 'label' => __('blog.title_field'), + 'required' => false, + 'maxLength' => 255, + ], + 'content' => [ + 'type' => 'richtext', + 'label' => __('blog.content_field'), + 'required' => false, + ], + 'excerpt' => [ + 'type' => 'textarea', + 'label' => __('blog.excerpt_field'), + 'required' => false, + 'maxLength' => 500, + 'rows' => 3, + ], + 'meta_title' => [ + 'type' => 'text', + 'label' => __('blog.meta_title_field'), + 'required' => false, + 'maxLength' => 60, + ], + 'meta_description' => [ + 'type' => 'textarea', + 'label' => __('blog.meta_description_field'), + 'required' => false, + 'maxLength' => 160, + 'rows' => 3, + ], + ]), + ]) + ->columnSpanFull() + ->collapsible(true) + ->collapsed(false), ]); } } diff --git a/app/Filament/Admin/Resources/Components/TranslationTabs.php b/app/Filament/Admin/Resources/Components/TranslationTabs.php new file mode 100644 index 0000000..b0c61c9 --- /dev/null +++ b/app/Filament/Admin/Resources/Components/TranslationTabs.php @@ -0,0 +1,217 @@ +ordered()->get(); + + if ($languages->isEmpty()) { + return Tabs::make('translations'); + } + + $tabs = []; + + foreach ($languages as $language) { + // User permission kontrolü + $canManage = auth()->user()?->canManageLanguage($language->code) ?? true; + + if (!$canManage) { + continue; + } + + $tabFields = []; + + foreach ($fields as $fieldName => $fieldConfig) { + $fieldType = $fieldConfig['type'] ?? 'text'; + $label = $fieldConfig['label'] ?? ucfirst($fieldName); + $required = $fieldConfig['required'] ?? false; + $maxLength = $fieldConfig['maxLength'] ?? null; + $rows = $fieldConfig['rows'] ?? 3; + + $fieldKey = "translations.{$language->code}.{$fieldName}"; + + switch ($fieldType) { + case 'richtext': + $field = RichEditor::make($fieldKey) + ->label($label) + ->fileAttachmentsDisk('public') + ->fileAttachmentsDirectory('translations') + ->fileAttachmentsVisibility('public') + ->toolbarButtons([ + 'attachFiles', + 'blockquote', + 'bold', + 'bulletList', + 'codeBlock', + 'h2', + 'h3', + 'italic', + 'link', + 'orderedList', + 'redo', + 'strike', + 'underline', + 'undo', + ]) + ->columnSpanFull() + ->required($required); + break; + + case 'textarea': + $field = Textarea::make($fieldKey) + ->label($label) + ->rows($rows) + ->columnSpanFull(); + break; + + case 'text': + default: + $field = TextInput::make($fieldKey) + ->label($label); + break; + } + + if ($required) { + $field->required(); + } + + if ($maxLength) { + $field->maxLength($maxLength); + } + + $tabFields[] = $field; + } + + // Convert country code to flag emoji + $code = strtoupper($language->code); + $flag = ''; + for ($i = 0; $i < strlen($code); $i++) { + $flag .= mb_chr(127397 + ord($code[$i])); + } + + $tabs[] = Tab::make($language->name) + ->label($flag . ' ' . $language->native_name) + ->schema($tabFields) + ->badge(fn ($record) => $record + ? self::getTranslationBadge($record, $language->code) + : null + ) + ->badgeColor(fn ($record) => $record + ? self::getTranslationBadgeColor($record, $language->code) + : 'gray' + ); + } + + return Tabs::make('translations') + ->tabs($tabs) + ->columnSpanFull() + ->persistTabInQueryString(); + } + + protected static function getTranslationBadge($record, string $languageCode): ?string + { + if (!method_exists($record, 'getTranslationProgress')) { + return null; + } + + $progress = $record->getTranslationProgress(); + $percentage = $progress[$languageCode] ?? 0; + + if ($percentage === 100) { + return '✓'; + } elseif ($percentage > 0) { + return $percentage . '%'; + } + + return null; + } + + protected static function getTranslationBadgeColor($record, string $languageCode): string + { + if (!method_exists($record, 'getTranslationProgress')) { + return 'gray'; + } + + $progress = $record->getTranslationProgress(); + $percentage = $progress[$languageCode] ?? 0; + + if ($percentage === 100) { + return 'success'; + } elseif ($percentage >= 50) { + return 'warning'; + } elseif ($percentage > 0) { + return 'danger'; + } + + return 'gray'; + } + + public static function fillFromRecord($record): array + { + $data = ['translations' => []]; + + if (!method_exists($record, 'getTranslatableFields')) { + return $data; + } + + $languages = Language::active()->ordered()->get(); + $fields = $record->getTranslatableFields(); + + foreach ($languages as $language) { + $data['translations'][$language->code] = []; + foreach ($fields as $field) { + $translation = $record->getTranslation($field, $language->code, false); + if ($translation) { + $data['translations'][$language->code][$field] = $translation->field_value; + } + } + } + + return $data; + } + + public static function saveTranslations($record, array $data): void + { + if (!method_exists($record, 'setTranslation')) { + return; + } + + $translations = $data['translations'] ?? []; + + \Log::info('Saving translations', ['data' => $translations]); + + foreach ($translations as $languageCode => $fields) { + if (!is_array($fields)) { + continue; + } + + foreach ($fields as $fieldName => $value) { + // Boş değerleri de kaydet (silme işlemi için) + $record->setTranslation( + $fieldName, + $languageCode, + $value ?? '', + 'published', // Default status + auth()->id() + ); + + \Log::info('Translation saved', [ + 'language' => $languageCode, + 'field' => $fieldName, + 'value' => substr($value ?? '', 0, 50) + ]); + } + } + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php b/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php index 199a18e..d6a37d2 100644 --- a/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php +++ b/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Pages\Pages; +use App\Filament\Admin\Resources\Components\TranslationTabs; use App\Filament\Admin\Resources\Pages\PageResource; use Filament\Resources\Pages\CreateRecord; @@ -23,4 +24,10 @@ class CreatePage extends CreateRecord { return __('pages.created_successfully'); } + + protected function afterCreate(): void + { + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php index a8cb597..569c930 100644 --- a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php +++ b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Pages\Pages; +use App\Filament\Admin\Resources\Components\TranslationTabs; use App\Filament\Admin\Resources\Pages\PageResource; use Filament\Actions\DeleteAction; use Filament\Actions\ForceDeleteAction; @@ -38,4 +39,18 @@ class EditPage extends EditRecord { return __('pages.updated_successfully'); } + + protected function mutateFormDataBeforeFill(array $data): array + { + // Load existing translations + $translationData = TranslationTabs::fillFromRecord($this->record); + + return array_merge($data, $translationData); + } + + protected function afterSave(): void + { + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php index 342a427..ceebbff 100644 --- a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php +++ b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Pages\Schemas; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\FileUpload; @@ -162,6 +163,47 @@ class PageForm ->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), ]); } } diff --git a/lang/en/blog.php b/lang/en/blog.php index 6c38691..8d98641 100644 --- a/lang/en/blog.php +++ b/lang/en/blog.php @@ -34,6 +34,7 @@ return [ 'content_section' => 'Content', 'settings_section' => 'Blog Settings', 'seo_section' => 'SEO Settings', + 'translations_section' => 'Translations', // Helper texts 'slug_helper' => 'The part that will appear in the URL. Example: blog-post', diff --git a/lang/en/pages.php b/lang/en/pages.php index 85e79c5..d70fc7a 100644 --- a/lang/en/pages.php +++ b/lang/en/pages.php @@ -26,6 +26,7 @@ return [ 'form_section_content' => 'Content', 'form_section_page_settings' => 'Page Settings', 'form_section_seo_settings' => 'SEO Settings', + 'translations_section' => 'Translations', // Form fields 'title_field' => 'Title', diff --git a/lang/tr/blog.php b/lang/tr/blog.php index 53cf6a6..a16d84f 100644 --- a/lang/tr/blog.php +++ b/lang/tr/blog.php @@ -34,6 +34,7 @@ return [ 'content_section' => 'İçerik', 'settings_section' => 'Blog Ayarları', 'seo_section' => 'SEO Ayarları', + 'translations_section' => 'Çeviriler', // Helper texts 'slug_helper' => 'URL\'de görünecek kısım. Örnek: blog-yazisi', diff --git a/lang/tr/pages.php b/lang/tr/pages.php index e3e2922..2b020aa 100644 --- a/lang/tr/pages.php +++ b/lang/tr/pages.php @@ -26,6 +26,7 @@ return [ 'form_section_content' => 'İçerik', 'form_section_page_settings' => 'Sayfa Ayarları', 'form_section_seo_settings' => 'SEO Ayarları', + 'translations_section' => 'Çeviriler', // Form fields 'title_field' => 'Başlık',