From 79a1f5b746b0267ac72c98e4d58e6ee07f4d3e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 29 Sep 2025 10:20:40 -0300 Subject: [PATCH] Add Blog module to Citrus Platform: Created Blog resource with CRUD pages, schemas, and models. Implemented localization for English and Turkish, ensuring consistent user experience. Added migrations for blog categories, posts, and comments, including soft delete functionality. Enhanced table and form structures for better organization and usability. --- .cursorrules | 11 ++ .../Admin/Resources/Blogs/BlogResource.php | 73 +++++++++ .../Resources/Blogs/Pages/CreateBlog.php | 26 +++ .../Admin/Resources/Blogs/Pages/EditBlog.php | 41 +++++ .../Admin/Resources/Blogs/Pages/ListBlogs.php | 25 +++ .../Resources/Blogs/Schemas/BlogForm.php | 154 ++++++++++++++++++ .../Resources/Blogs/Tables/BlogsTable.php | 126 ++++++++++++++ .../Resources/Pages/Schemas/PageForm.php | 70 ++++---- .../Resources/Pages/Tables/PagesTable.php | 50 +++--- app/Models/Blog.php | 102 ++++++++++++ app/Models/BlogCategory.php | 46 ++++++ app/Models/BlogComment.php | 57 +++++++ ...29_114551_create_blog_categories_table.php | 36 ++++ .../2025_09_29_114552_create_blogs_table.php | 47 ++++++ ...9_29_114633_create_blog_comments_table.php | 39 +++++ ...9_115350_add_deleted_at_to_blogs_table.php | 34 ++++ lang/en/blog.php | 78 +++++++++ lang/en/pages.php | 61 ++++++- lang/tr/blog.php | 78 +++++++++ lang/tr/pages.php | 61 ++++++- 20 files changed, 1145 insertions(+), 70 deletions(-) create mode 100644 app/Filament/Admin/Resources/Blogs/BlogResource.php create mode 100644 app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php create mode 100644 app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php create mode 100644 app/Filament/Admin/Resources/Blogs/Pages/ListBlogs.php create mode 100644 app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php create mode 100644 app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php create mode 100644 app/Models/Blog.php create mode 100644 app/Models/BlogCategory.php create mode 100644 app/Models/BlogComment.php create mode 100644 database/migrations/2025_09_29_114551_create_blog_categories_table.php create mode 100644 database/migrations/2025_09_29_114552_create_blogs_table.php create mode 100644 database/migrations/2025_09_29_114633_create_blog_comments_table.php create mode 100644 database/migrations/2025_09_29_115350_add_deleted_at_to_blogs_table.php create mode 100644 lang/en/blog.php create mode 100644 lang/tr/blog.php diff --git a/.cursorrules b/.cursorrules index 65e70e3..cb20757 100644 --- a/.cursorrules +++ b/.cursorrules @@ -25,6 +25,15 @@ app/Filament/Admin/Resources/ModuleName/ └── ModuleNameTable.php ``` +### Filament Resource Oluşturma +- Yeni modül oluştururken Filament'in kendi komutunu kullan: +```bash +php artisan make:filament-resource ModelName --generate --model --migration --factory +``` +- Bu komut otomatik olarak tüm gerekli dosyaları oluşturur +- Manuel dosya oluşturma yerine bu komutu tercih et +- Komut çalıştırıldıktan sonra model ve migration'ları ihtiyaca göre düzenle + ### Resource Sınıfı Kuralları - `getNavigationLabel()`, `getModelLabel()`, `getPluralModelLabel()` metodlarını implement et - Tüm metinler için localization kullan @@ -87,6 +96,8 @@ return [ - Foreign key'ler için `_id` suffix'i kullan - Timestamps için `created_at` ve `updated_at` kullan - Soft delete için `deleted_at` kullan +- Mevcut tablolar için migration oluştururken `if (!Schema::hasTable())` kontrolü kullanma +- Soft delete hatası alındığında `deleted_at` sütununu ekleyen ayrı migration oluştur ### Test Kuralları - Her modül için test dosyaları oluştur diff --git a/app/Filament/Admin/Resources/Blogs/BlogResource.php b/app/Filament/Admin/Resources/Blogs/BlogResource.php new file mode 100644 index 0000000..25fa0b9 --- /dev/null +++ b/app/Filament/Admin/Resources/Blogs/BlogResource.php @@ -0,0 +1,73 @@ + ListBlogs::route('/'), + 'create' => CreateBlog::route('/create'), + 'edit' => EditBlog::route('/{record}/edit'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php new file mode 100644 index 0000000..fbd5dad --- /dev/null +++ b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php @@ -0,0 +1,26 @@ +getResource()::getUrl('index'); + } + + protected function getCreatedNotificationTitle(): ?string + { + return __('blog.created_successfully'); + } +} diff --git a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php new file mode 100644 index 0000000..af1a630 --- /dev/null +++ b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php @@ -0,0 +1,41 @@ +label(__('blog.delete')), + RestoreAction::make() + ->label(__('blog.restore')), + ForceDeleteAction::make() + ->label(__('blog.force_delete')), + ]; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } + + protected function getSavedNotificationTitle(): ?string + { + return __('blog.updated_successfully'); + } +} diff --git a/app/Filament/Admin/Resources/Blogs/Pages/ListBlogs.php b/app/Filament/Admin/Resources/Blogs/Pages/ListBlogs.php new file mode 100644 index 0000000..490cc27 --- /dev/null +++ b/app/Filament/Admin/Resources/Blogs/Pages/ListBlogs.php @@ -0,0 +1,25 @@ +label(__('blog.create')), + ]; + } +} diff --git a/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php new file mode 100644 index 0000000..13c0a6c --- /dev/null +++ b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php @@ -0,0 +1,154 @@ +columns(3) + ->schema([ + // Sol kolon - Ana içerik (2 sütun genişliğinde) + Section::make(__('blog.content_section')) + ->schema([ + TextInput::make('title') + ->label(__('blog.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(__('blog.slug_field')) + ->required() + ->maxLength(255) + ->unique(ignoreRecord: true) + ->rules(['alpha_dash']) + ->helperText(__('blog.slug_helper')), + + RichEditor::make('content') + ->label(__('blog.content_field')) + ->required() + ->fileAttachmentsDisk('public') + ->fileAttachmentsDirectory('blogs') + ->fileAttachmentsVisibility('public') + ->columnSpanFull(), + + Textarea::make('excerpt') + ->label(__('blog.excerpt_field')) + ->rows(3) + ->maxLength(500) + ->helperText(__('blog.excerpt_helper')) + ->columnSpanFull(), + ]) + ->columnSpan(2) + ->collapsible(false), + + // Sağ kolon - Metadata ve öne çıkan görsel (1 sütun genişliğinde) + Section::make(__('blog.settings_section')) + ->schema([ + FileUpload::make('featured_image') + ->label(__('blog.featured_image_field')) + ->image() + ->disk('public') + ->directory('blogs/featured') + ->visibility('public') + ->imageEditor() + ->imageEditorAspectRatios([ + '16:9', + '4:3', + '1:1', + ]) + ->helperText(__('blog.featured_image_helper')) + ->columnSpanFull(), + + Select::make('author_id') + ->label(__('blog.author_field')) + ->relationship('author', 'name') + ->default(auth()->id()) + ->required() + ->columnSpanFull(), + + Select::make('category_id') + ->label(__('blog.category_field')) + ->relationship('category', 'name') + ->searchable() + ->preload() + ->helperText(__('blog.category_helper')) + ->columnSpanFull(), + + TagsInput::make('tags') + ->label(__('blog.tags_field')) + ->helperText(__('blog.tags_helper')) + ->columnSpanFull(), + + DateTimePicker::make('published_at') + ->label(__('blog.published_at_field')) + ->displayFormat('d.m.Y H:i') + ->helperText(__('blog.published_at_helper')) + ->columnSpanFull(), + + Select::make('status') + ->label(__('blog.status_field')) + ->options([ + 'draft' => __('blog.status_draft'), + 'published' => __('blog.status_published'), + 'archived' => __('blog.status_archived'), + ]) + ->default('draft') + ->required() + ->columnSpanFull(), + + Checkbox::make('is_featured') + ->label(__('blog.is_featured_field')) + ->helperText(__('blog.is_featured_helper')) + ->columnSpanFull(), + + Checkbox::make('allow_comments') + ->label(__('blog.allow_comments_field')) + ->default(true) + ->helperText(__('blog.allow_comments_helper')) + ->columnSpanFull(), + ]) + ->columnSpan(1) + ->collapsible(false), + + // Alt kısım - SEO ayarları (tam genişlik) + Section::make(__('blog.seo_section')) + ->schema([ + TextInput::make('meta_title') + ->label(__('blog.meta_title_field')) + ->maxLength(60) + ->helperText(__('blog.meta_title_helper')), + + Textarea::make('meta_description') + ->label(__('blog.meta_description_field')) + ->rows(3) + ->maxLength(160) + ->helperText(__('blog.meta_description_helper')) + ->columnSpanFull(), + ]) + ->columns(2) + ->columnSpanFull() + ->collapsible(true) + ->collapsed(true), + ]); + } +} diff --git a/app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php b/app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php new file mode 100644 index 0000000..db9b75d --- /dev/null +++ b/app/Filament/Admin/Resources/Blogs/Tables/BlogsTable.php @@ -0,0 +1,126 @@ +columns([ + ImageColumn::make('featured_image') + ->label(__('blog.featured_image_field')) + ->disk('public') + ->square() + ->size(60), + + TextColumn::make('title') + ->label(__('blog.table_title')) + ->searchable() + ->sortable() + ->limit(50), + + TextColumn::make('slug') + ->label(__('blog.table_slug')) + ->searchable() + ->sortable() + ->limit(30), + + TextColumn::make('status') + ->label(__('blog.table_status')) + ->badge() + ->color(fn (string $state): string => match ($state) { + 'draft' => 'gray', + 'published' => 'success', + 'archived' => 'warning', + }) + ->formatStateUsing(fn (string $state): string => match ($state) { + 'draft' => __('blog.status_draft'), + 'published' => __('blog.status_published'), + 'archived' => __('blog.status_archived'), + }), + + TextColumn::make('author.name') + ->label(__('blog.table_author')) + ->searchable() + ->sortable(), + + TextColumn::make('category.name') + ->label(__('blog.table_category')) + ->searchable() + ->sortable(), + + TextColumn::make('published_at') + ->label(__('blog.table_published_at')) + ->dateTime('d.m.Y H:i') + ->sortable(), + + TextColumn::make('view_count') + ->label(__('blog.table_view_count')) + ->sortable() + ->alignCenter(), + + ToggleColumn::make('is_featured') + ->label(__('blog.is_featured_field')) + ->alignCenter(), + + TextColumn::make('created_at') + ->label(__('blog.table_created_at')) + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('updated_at') + ->label(__('blog.table_updated_at')) + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + SelectFilter::make('status') + ->label(__('blog.status_field')) + ->options([ + 'draft' => __('blog.status_draft'), + 'published' => __('blog.status_published'), + 'archived' => __('blog.status_archived'), + ]), + + SelectFilter::make('category_id') + ->label(__('blog.category_field')) + ->relationship('category', 'name'), + + TernaryFilter::make('is_featured') + ->label(__('blog.is_featured_field')), + + TernaryFilter::make('allow_comments') + ->label(__('blog.allow_comments_field')), + ]) + ->recordActions([ + EditAction::make() + ->label(__('blog.edit')), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make() + ->label(__('blog.delete')), + RestoreBulkAction::make() + ->label(__('blog.restore')), + ForceDeleteBulkAction::make() + ->label(__('blog.force_delete')), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } +} diff --git a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php index f71e9d9..342a427 100644 --- a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php +++ b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php @@ -21,10 +21,10 @@ class PageForm ->columns(3) ->schema([ // Sol kolon - Ana içerik (2 sütun genişliğinde) - Section::make('İçerik') + Section::make(__('pages.form_section_content')) ->schema([ TextInput::make('title') - ->label('Başlık') + ->label(__('pages.title_field')) ->required() ->maxLength(255) ->live(onBlur: true) @@ -36,15 +36,15 @@ class PageForm }), TextInput::make('slug') - ->label('URL Slug') + ->label(__('pages.slug_field')) ->required() ->maxLength(255) ->unique(ignoreRecord: true) ->rules(['alpha_dash']) - ->helperText('URL\'de görünecek kısım. Örnek: hakkimizda'), + ->helperText(__('pages.slug_helper_text')), RichEditor::make('content') - ->label('İçerik') + ->label(__('pages.content_field')) ->required() ->fileAttachmentsDisk('public') ->fileAttachmentsDirectory('pages') @@ -52,20 +52,20 @@ class PageForm ->columnSpanFull(), Textarea::make('excerpt') - ->label('Özet') + ->label(__('pages.excerpt_field')) ->rows(3) ->maxLength(500) - ->helperText('Sayfa özeti (maksimum 500 karakter)') + ->helperText(__('pages.excerpt_helper_text')) ->columnSpanFull(), ]) ->columnSpan(2) ->collapsible(false), // Sağ kolon - Metadata ve öne çıkan görsel (1 sütun genişliğinde) - Section::make('Sayfa Ayarları') + Section::make(__('pages.form_section_page_settings')) ->schema([ FileUpload::make('featured_image') - ->label('Öne Çıkan Görsel') + ->label(__('pages.featured_image_field')) ->image() ->disk('public') ->directory('pages/featured') @@ -76,86 +76,86 @@ class PageForm '4:3', '1:1', ]) - ->helperText('Sayfa için öne çıkan görsel seçin') + ->helperText(__('pages.featured_image_helper_text')) ->columnSpanFull(), Select::make('author_id') - ->label('Yazar') + ->label(__('pages.author_field')) ->relationship('author', 'name') ->default(auth()->id()) ->required() ->columnSpanFull(), DateTimePicker::make('published_at') - ->label('Yayın Tarihi') + ->label(__('pages.published_at_field')) ->displayFormat('d.m.Y H:i') - ->helperText('Boş bırakılırsa şu anki tarih kullanılır') + ->helperText(__('pages.published_at_helper_text')) ->columnSpanFull(), Select::make('status') - ->label('Durum') + ->label(__('pages.status_field')) ->options([ - 'draft' => 'Taslak', - 'published' => 'Yayında', - 'archived' => 'Arşivlendi', + 'draft' => __('pages.status_draft'), + 'published' => __('pages.status_published'), + 'archived' => __('pages.status_archived'), ]) ->default('draft') ->required() ->columnSpanFull(), Select::make('parent_id') - ->label('Üst Sayfa') + ->label(__('pages.parent_field')) ->relationship('parent', 'title') ->searchable() ->preload() - ->helperText('Bu sayfayı başka bir sayfanın alt sayfası yapmak için seçin') + ->helperText(__('pages.parent_helper_text')) ->columnSpanFull(), Select::make('template') - ->label('Şablon') + ->label(__('pages.template_field')) ->options([ - 'default' => 'Varsayılan', - 'landing' => 'Landing Page', - 'blog' => 'Blog', - 'contact' => 'İletişim', + 'default' => __('pages.template_default'), + 'landing' => __('pages.template_landing'), + 'blog' => __('pages.template_blog'), + 'contact' => __('pages.template_contact'), ]) ->default('default') ->columnSpanFull(), TextInput::make('sort_order') - ->label('Sıra') + ->label(__('pages.sort_order_field')) ->numeric() ->default(0) - ->helperText('Menüde görünme sırası') + ->helperText(__('pages.sort_order_helper_text')) ->columnSpanFull(), Checkbox::make('is_homepage') - ->label('Ana Sayfa') - ->helperText('Bu sayfayı ana sayfa olarak ayarla') + ->label(__('pages.is_homepage_field')) + ->helperText(__('pages.is_homepage_helper_text')) ->columnSpanFull(), Checkbox::make('show_in_menu') - ->label('Menüde Göster') + ->label(__('pages.show_in_menu_field')) ->default(true) - ->helperText('Bu sayfa menüde görünsün mü?') + ->helperText(__('pages.show_in_menu_helper_text')) ->columnSpanFull(), ]) ->columnSpan(1) ->collapsible(false), // Alt kısım - SEO ayarları (tam genişlik) - Section::make('SEO Ayarları') + Section::make(__('pages.form_section_seo_settings')) ->schema([ TextInput::make('meta_title') - ->label('Meta Başlık') + ->label(__('pages.meta_title_field')) ->maxLength(60) - ->helperText('Arama motorları için başlık (maksimum 60 karakter)'), + ->helperText(__('pages.meta_title_helper_text')), Textarea::make('meta_description') - ->label('Meta Açıklama') + ->label(__('pages.meta_description_field')) ->rows(3) ->maxLength(160) - ->helperText('Arama motorları için açıklama (maksimum 160 karakter)') + ->helperText(__('pages.meta_description_helper_text')) ->columnSpanFull(), ]) ->columns(2) diff --git a/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php b/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php index 2396120..54bae22 100644 --- a/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php +++ b/app/Filament/Admin/Resources/Pages/Tables/PagesTable.php @@ -22,29 +22,29 @@ class PagesTable return $table ->columns([ ImageColumn::make('featured_image_url') - ->label('Görsel') + ->label(__('pages.table_column_featured_image')) ->circular() ->size(40) ->defaultImageUrl('/images/placeholder-page.png'), TextColumn::make('title') - ->label('Başlık') + ->label(__('pages.table_column_title')) ->searchable() ->sortable() ->weight('bold') ->description(fn ($record) => $record->excerpt ? \Str::limit($record->excerpt, 50) : null), TextColumn::make('slug') - ->label('URL') + ->label(__('pages.table_column_slug')) ->searchable() ->sortable() ->copyable() - ->copyMessage('URL kopyalandı') + ->copyMessage(__('pages.copy_url_message')) ->url(fn ($record) => $record->url, shouldOpenInNewTab: true) ->color('primary'), TextColumn::make('status') - ->label('Durum') + ->label(__('pages.table_column_status')) ->badge() ->color(fn (string $state): string => match ($state) { 'published' => 'success', @@ -52,70 +52,70 @@ class PagesTable 'archived' => 'gray', }) ->formatStateUsing(fn (string $state): string => match ($state) { - 'published' => 'Yayında', - 'draft' => 'Taslak', - 'archived' => 'Arşivlendi', + 'published' => __('pages.status_published'), + 'draft' => __('pages.status_draft'), + 'archived' => __('pages.status_archived'), }), TextColumn::make('author.name') - ->label('Yazar') + ->label(__('pages.table_column_author')) ->searchable() ->sortable() ->toggleable(), TextColumn::make('published_at') - ->label('Yayın Tarihi') + ->label(__('pages.table_column_published_at')) ->dateTime('d.m.Y H:i') ->sortable() ->toggleable(), TextColumn::make('parent.title') - ->label('Üst Sayfa') + ->label(__('pages.table_column_parent')) ->searchable() ->sortable() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('template') - ->label('Şablon') + ->label(__('pages.table_column_template')) ->badge() ->color('info') ->toggleable(isToggledHiddenByDefault: true), ToggleColumn::make('is_homepage') - ->label('Ana Sayfa') + ->label(__('pages.table_column_is_homepage')) ->toggleable(isToggledHiddenByDefault: true), ToggleColumn::make('show_in_menu') - ->label('Menüde Göster') + ->label(__('pages.table_column_show_in_menu')) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('sort_order') - ->label('Sıra') + ->label(__('pages.table_column_sort_order')) ->sortable() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('created_at') - ->label('Oluşturulma') + ->label(__('pages.table_column_created_at')) ->dateTime('d.m.Y H:i') ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ SelectFilter::make('status') - ->label('Durum') + ->label(__('pages.table_column_status')) ->options([ - 'published' => 'Yayında', - 'draft' => 'Taslak', - 'archived' => 'Arşivlendi', + 'published' => __('pages.status_published'), + 'draft' => __('pages.status_draft'), + 'archived' => __('pages.status_archived'), ]), SelectFilter::make('template') - ->label('Şablon') + ->label(__('pages.table_column_template')) ->options([ - 'default' => 'Varsayılan', - 'landing' => 'Landing Page', - 'blog' => 'Blog', - 'contact' => 'İletişim', + 'default' => __('pages.template_default'), + 'landing' => __('pages.template_landing'), + 'blog' => __('pages.template_blog'), + 'contact' => __('pages.template_contact'), ]), TrashedFilter::make(), diff --git a/app/Models/Blog.php b/app/Models/Blog.php new file mode 100644 index 0000000..d53d08e --- /dev/null +++ b/app/Models/Blog.php @@ -0,0 +1,102 @@ + 'datetime', + 'tags' => 'array', + 'is_featured' => 'boolean', + 'allow_comments' => 'boolean', + 'view_count' => 'integer', + ]; + + public function author() + { + return $this->belongsTo(User::class, 'author_id'); + } + + public function category() + { + return $this->belongsTo(BlogCategory::class, 'category_id'); + } + + public function comments() + { + return $this->hasMany(BlogComment::class); + } + + // Route key için id kullan (Filament için gerekli) + // public function getRouteKeyName() + // { + // return 'slug'; + // } + + public function getUrlAttribute() + { + return '/blog/' . $this->slug; + } + + public function getFeaturedImageUrlAttribute() + { + if ($this->featured_image) { + return asset('storage/' . $this->featured_image); + } + + return null; + } + + public function getReadingTimeAttribute() + { + $wordCount = str_word_count(strip_tags($this->content)); + $minutesToRead = round($wordCount / 200); // Ortalama 200 kelime/dakika + + return max(1, $minutesToRead); + } + + public function scopePublished($query) + { + return $query->where('status', 'published') + ->where('published_at', '<=', now()); + } + + public function scopeFeatured($query) + { + return $query->where('is_featured', true); + } + + public function scopeByCategory($query, $categoryId) + { + return $query->where('category_id', $categoryId); + } + + public function scopeByTag($query, $tag) + { + return $query->whereJsonContains('tags', $tag); + } +} diff --git a/app/Models/BlogCategory.php b/app/Models/BlogCategory.php new file mode 100644 index 0000000..4b4bde3 --- /dev/null +++ b/app/Models/BlogCategory.php @@ -0,0 +1,46 @@ + 'integer', + 'is_active' => 'boolean', + ]; + + public function blogs() + { + return $this->hasMany(Blog::class); + } + + public function getRouteKeyName() + { + return 'slug'; + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function scopeOrdered($query) + { + return $query->orderBy('sort_order')->orderBy('name'); + } +} diff --git a/app/Models/BlogComment.php b/app/Models/BlogComment.php new file mode 100644 index 0000000..0853720 --- /dev/null +++ b/app/Models/BlogComment.php @@ -0,0 +1,57 @@ + 'integer', + ]; + + public function blog() + { + return $this->belongsTo(Blog::class); + } + + public function parent() + { + return $this->belongsTo(BlogComment::class, 'parent_id'); + } + + public function replies() + { + return $this->hasMany(BlogComment::class, 'parent_id'); + } + + public function scopeApproved($query) + { + return $query->where('status', 'approved'); + } + + public function scopePending($query) + { + return $query->where('status', 'pending'); + } + + public function scopeSpam($query) + { + return $query->where('status', 'spam'); + } +} diff --git a/database/migrations/2025_09_29_114551_create_blog_categories_table.php b/database/migrations/2025_09_29_114551_create_blog_categories_table.php new file mode 100644 index 0000000..74cf0eb --- /dev/null +++ b/database/migrations/2025_09_29_114551_create_blog_categories_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->string('color', 7)->default('#3B82F6'); // Hex color + $table->integer('sort_order')->default(0); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['is_active', 'sort_order']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_categories'); + } +}; diff --git a/database/migrations/2025_09_29_114552_create_blogs_table.php b/database/migrations/2025_09_29_114552_create_blogs_table.php new file mode 100644 index 0000000..b1ed1af --- /dev/null +++ b/database/migrations/2025_09_29_114552_create_blogs_table.php @@ -0,0 +1,47 @@ +id(); + $table->string('title'); + $table->string('slug')->unique(); + $table->longText('content'); + $table->text('excerpt')->nullable(); + $table->string('meta_title')->nullable(); + $table->text('meta_description')->nullable(); + $table->enum('status', ['draft', 'published', 'archived'])->default('draft'); + $table->string('featured_image')->nullable(); + $table->timestamp('published_at')->nullable(); + $table->foreignId('author_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('category_id')->nullable()->constrained('blog_categories')->onDelete('set null'); + $table->json('tags')->nullable(); + $table->integer('view_count')->default(0); + $table->boolean('is_featured')->default(false); + $table->boolean('allow_comments')->default(true); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['status', 'published_at']); + $table->index(['category_id', 'status']); + $table->index(['is_featured', 'status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blogs'); + } +}; diff --git a/database/migrations/2025_09_29_114633_create_blog_comments_table.php b/database/migrations/2025_09_29_114633_create_blog_comments_table.php new file mode 100644 index 0000000..628066e --- /dev/null +++ b/database/migrations/2025_09_29_114633_create_blog_comments_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('blog_id')->constrained('blogs')->onDelete('cascade'); + $table->string('name'); + $table->string('email'); + $table->text('content'); + $table->enum('status', ['pending', 'approved', 'spam', 'rejected'])->default('pending'); + $table->foreignId('parent_id')->nullable()->constrained('blog_comments')->onDelete('cascade'); + $table->string('ip_address')->nullable(); + $table->text('user_agent')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['blog_id', 'status']); + $table->index(['status', 'created_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('blog_comments'); + } +}; diff --git a/database/migrations/2025_09_29_115350_add_deleted_at_to_blogs_table.php b/database/migrations/2025_09_29_115350_add_deleted_at_to_blogs_table.php new file mode 100644 index 0000000..02fab6c --- /dev/null +++ b/database/migrations/2025_09_29_115350_add_deleted_at_to_blogs_table.php @@ -0,0 +1,34 @@ +softDeletes(); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('blogs', function (Blueprint $table) { + // deleted_at sütununu sil + if (Schema::hasColumn('blogs', 'deleted_at')) { + $table->dropSoftDeletes(); + } + }); + } +}; diff --git a/lang/en/blog.php b/lang/en/blog.php new file mode 100644 index 0000000..6c38691 --- /dev/null +++ b/lang/en/blog.php @@ -0,0 +1,78 @@ + 'Blog Posts', + 'navigation_label' => 'Blog', + 'model_label' => 'Blog Post', + 'plural_model_label' => 'Blog Posts', + + // Actions + 'create' => 'Create New Blog Post', + 'edit' => 'Edit Blog Post', + 'delete' => 'Delete Blog Post', + 'restore' => 'Restore Blog Post', + 'force_delete' => 'Force Delete', + + // Form fields + 'title_field' => 'Title', + 'slug_field' => 'URL Slug', + 'content_field' => 'Content', + 'excerpt_field' => 'Excerpt', + 'meta_title_field' => 'Meta Title', + 'meta_description_field' => 'Meta Description', + 'status_field' => 'Status', + 'featured_image_field' => 'Featured Image', + 'published_at_field' => 'Published At', + 'author_field' => 'Author', + 'category_field' => 'Category', + 'tags_field' => 'Tags', + 'view_count_field' => 'View Count', + 'is_featured_field' => 'Featured', + 'allow_comments_field' => 'Allow Comments', + + // Sections + 'content_section' => 'Content', + 'settings_section' => 'Blog Settings', + 'seo_section' => 'SEO Settings', + + // Helper texts + 'slug_helper' => 'The part that will appear in the URL. Example: blog-post', + 'excerpt_helper' => 'Blog post excerpt (maximum 500 characters)', + 'featured_image_helper' => 'Select a featured image for the blog post', + 'category_helper' => 'Assign the blog post to a category', + 'tags_helper' => 'Add tags for the blog post', + 'published_at_helper' => 'If left blank, current date will be used', + 'is_featured_helper' => 'Mark this blog post as featured', + 'allow_comments_helper' => 'Allow comments on this blog post', + 'meta_title_helper' => 'Title for search engines (maximum 60 characters)', + 'meta_description_helper' => 'Description for search engines (maximum 160 characters)', + + // Status options + 'status_draft' => 'Draft', + 'status_published' => 'Published', + 'status_archived' => 'Archived', + + // Messages + 'created_successfully' => 'Blog post created successfully.', + 'updated_successfully' => 'Blog post updated successfully.', + 'deleted_successfully' => 'Blog post deleted successfully.', + 'restored_successfully' => 'Blog post restored successfully.', + + // Table columns + 'table_title' => 'Title', + 'table_slug' => 'URL Slug', + 'table_status' => 'Status', + 'table_author' => 'Author', + 'table_category' => 'Category', + 'table_published_at' => 'Published At', + 'table_view_count' => 'Views', + 'table_created_at' => 'Created At', + 'table_updated_at' => 'Updated At', + + // Validation messages + 'title_required' => 'Title field is required.', + 'slug_required' => 'URL slug field is required.', + 'slug_unique' => 'This URL slug is already in use.', + 'content_required' => 'Content field is required.', + 'author_required' => 'Author field is required.', +]; diff --git a/lang/en/pages.php b/lang/en/pages.php index 78aea0a..85e79c5 100644 --- a/lang/en/pages.php +++ b/lang/en/pages.php @@ -22,6 +22,40 @@ return [ 'status_field' => 'Status', 'published_at_field' => 'Published At', + // Form sections + 'form_section_content' => 'Content', + 'form_section_page_settings' => 'Page Settings', + 'form_section_seo_settings' => 'SEO Settings', + + // Form fields + 'title_field' => 'Title', + 'slug_field' => 'URL Slug', + 'content_field' => 'Content', + 'excerpt_field' => 'Excerpt', + 'featured_image_field' => 'Featured Image', + 'author_field' => 'Author', + 'published_at_field' => 'Published At', + 'status_field' => 'Status', + 'parent_field' => 'Parent Page', + 'template_field' => 'Template', + 'sort_order_field' => 'Sort Order', + 'is_homepage_field' => 'Homepage', + 'show_in_menu_field' => 'Show in Menu', + 'meta_title_field' => 'Meta Title', + 'meta_description_field' => 'Meta Description', + + // Helper texts + 'slug_helper_text' => 'The part that will appear in the URL. Example: about-us', + 'excerpt_helper_text' => 'Page excerpt (maximum 500 characters)', + 'featured_image_helper_text' => 'Select a featured image for the page', + 'published_at_helper_text' => 'If left empty, current date will be used', + 'parent_helper_text' => 'Select to make this page a sub-page of another page', + 'sort_order_helper_text' => 'Display order in menu', + 'is_homepage_helper_text' => 'Set this page as the homepage', + 'show_in_menu_helper_text' => 'Should this page be shown in menu?', + 'meta_title_helper_text' => 'Title for search engines (maximum 60 characters)', + 'meta_description_helper_text' => 'Description for search engines (maximum 160 characters)', + // Status options 'status_draft' => 'Draft', 'status_published' => 'Published', @@ -34,11 +68,28 @@ return [ 'restored_successfully' => 'Page restored successfully.', // Table columns - 'table_title' => 'Title', - 'table_slug' => 'URL Slug', - 'table_status' => 'Status', - 'table_created_at' => 'Created At', - 'table_updated_at' => 'Updated At', + 'table_column_featured_image' => 'Featured Image', + 'table_column_title' => 'Title', + 'table_column_slug' => 'URL Slug', + 'table_column_status' => 'Status', + 'table_column_author' => 'Author', + 'table_column_published_at' => 'Published At', + 'table_column_parent' => 'Parent Page', + 'table_column_template' => 'Template', + 'table_column_is_homepage' => 'Homepage', + 'table_column_show_in_menu' => 'Show in Menu', + 'table_column_sort_order' => 'Sort Order', + 'table_column_created_at' => 'Created At', + 'table_column_updated_at' => 'Updated At', + + // Template options + 'template_default' => 'Default', + 'template_landing' => 'Landing Page', + 'template_blog' => 'Blog', + 'template_contact' => 'Contact', + + // Copy message + 'copy_url_message' => 'URL copied', // Validation messages 'title_required' => 'The title field is required.', diff --git a/lang/tr/blog.php b/lang/tr/blog.php new file mode 100644 index 0000000..53cf6a6 --- /dev/null +++ b/lang/tr/blog.php @@ -0,0 +1,78 @@ + 'Blog Yazıları', + 'navigation_label' => 'Blog', + 'model_label' => 'Blog Yazısı', + 'plural_model_label' => 'Blog Yazıları', + + // Actions + 'create' => 'Yeni Blog Yazısı Oluştur', + 'edit' => 'Blog Yazısını Düzenle', + 'delete' => 'Blog Yazısını Sil', + 'restore' => 'Blog Yazısını Geri Yükle', + 'force_delete' => 'Kalıcı Olarak Sil', + + // Form fields + 'title_field' => 'Başlık', + 'slug_field' => 'URL Yolu', + 'content_field' => 'İçerik', + 'excerpt_field' => 'Özet', + 'meta_title_field' => 'Meta Başlık', + 'meta_description_field' => 'Meta Açıklama', + 'status_field' => 'Durum', + 'featured_image_field' => 'Öne Çıkan Görsel', + 'published_at_field' => 'Yayın Tarihi', + 'author_field' => 'Yazar', + 'category_field' => 'Kategori', + 'tags_field' => 'Etiketler', + 'view_count_field' => 'Görüntülenme Sayısı', + 'is_featured_field' => 'Öne Çıkan', + 'allow_comments_field' => 'Yorumlara İzin Ver', + + // Sections + 'content_section' => 'İçerik', + 'settings_section' => 'Blog Ayarları', + 'seo_section' => 'SEO Ayarları', + + // Helper texts + 'slug_helper' => 'URL\'de görünecek kısım. Örnek: blog-yazisi', + 'excerpt_helper' => 'Blog yazısı özeti (maksimum 500 karakter)', + 'featured_image_helper' => 'Blog yazısı için öne çıkan görsel seçin', + 'category_helper' => 'Blog yazısını bir kategoriye atayın', + 'tags_helper' => 'Blog yazısı için etiketler ekleyin', + 'published_at_helper' => 'Boş bırakılırsa şu anki tarih kullanılır', + 'is_featured_helper' => 'Bu blog yazısını öne çıkan olarak işaretle', + 'allow_comments_helper' => 'Bu blog yazısında yorumlara izin ver', + 'meta_title_helper' => 'Arama motorları için başlık (maksimum 60 karakter)', + 'meta_description_helper' => 'Arama motorları için açıklama (maksimum 160 karakter)', + + // Status options + 'status_draft' => 'Taslak', + 'status_published' => 'Yayınlandı', + 'status_archived' => 'Arşivlendi', + + // Messages + 'created_successfully' => 'Blog yazısı başarıyla oluşturuldu.', + 'updated_successfully' => 'Blog yazısı başarıyla güncellendi.', + 'deleted_successfully' => 'Blog yazısı başarıyla silindi.', + 'restored_successfully' => 'Blog yazısı başarıyla geri yüklendi.', + + // Table columns + 'table_title' => 'Başlık', + 'table_slug' => 'URL Yolu', + 'table_status' => 'Durum', + 'table_author' => 'Yazar', + 'table_category' => 'Kategori', + 'table_published_at' => 'Yayın Tarihi', + 'table_view_count' => 'Görüntülenme', + 'table_created_at' => 'Oluşturulma Tarihi', + 'table_updated_at' => 'Güncellenme Tarihi', + + // Validation messages + 'title_required' => 'Başlık alanı zorunludur.', + 'slug_required' => 'URL yolu alanı zorunludur.', + 'slug_unique' => 'Bu URL yolu zaten kullanılıyor.', + 'content_required' => 'İçerik alanı zorunludur.', + 'author_required' => 'Yazar alanı zorunludur.', +]; diff --git a/lang/tr/pages.php b/lang/tr/pages.php index 2d52627..e3e2922 100644 --- a/lang/tr/pages.php +++ b/lang/tr/pages.php @@ -22,6 +22,40 @@ return [ 'status_field' => 'Durum', 'published_at_field' => 'Yayın Tarihi', + // Form sections + 'form_section_content' => 'İçerik', + 'form_section_page_settings' => 'Sayfa Ayarları', + 'form_section_seo_settings' => 'SEO Ayarları', + + // Form fields + 'title_field' => 'Başlık', + 'slug_field' => 'URL Slug', + 'content_field' => 'İçerik', + 'excerpt_field' => 'Özet', + 'featured_image_field' => 'Öne Çıkan Görsel', + 'author_field' => 'Yazar', + 'published_at_field' => 'Yayın Tarihi', + 'status_field' => 'Durum', + 'parent_field' => 'Üst Sayfa', + 'template_field' => 'Şablon', + 'sort_order_field' => 'Sıra', + 'is_homepage_field' => 'Ana Sayfa', + 'show_in_menu_field' => 'Menüde Göster', + 'meta_title_field' => 'Meta Başlık', + 'meta_description_field' => 'Meta Açıklama', + + // Helper texts + 'slug_helper_text' => 'URL\'de görünecek kısım. Örnek: hakkimizda', + 'excerpt_helper_text' => 'Sayfa özeti (maksimum 500 karakter)', + 'featured_image_helper_text' => 'Sayfa için öne çıkan görsel seçin', + 'published_at_helper_text' => 'Boş bırakılırsa şu anki tarih kullanılır', + 'parent_helper_text' => 'Bu sayfayı başka bir sayfanın alt sayfası yapmak için seçin', + 'sort_order_helper_text' => 'Menüde görünme sırası', + 'is_homepage_helper_text' => 'Bu sayfayı ana sayfa olarak ayarla', + 'show_in_menu_helper_text' => 'Bu sayfa menüde görünsün mü?', + 'meta_title_helper_text' => 'Arama motorları için başlık (maksimum 60 karakter)', + 'meta_description_helper_text' => 'Arama motorları için açıklama (maksimum 160 karakter)', + // Status options 'status_draft' => 'Taslak', 'status_published' => 'Yayınlandı', @@ -34,11 +68,28 @@ return [ 'restored_successfully' => 'Sayfa başarıyla geri yüklendi.', // Table columns - 'table_title' => 'Başlık', - 'table_slug' => 'URL Yolu', - 'table_status' => 'Durum', - 'table_created_at' => 'Oluşturulma Tarihi', - 'table_updated_at' => 'Güncellenme Tarihi', + 'table_column_featured_image' => 'Öne Çıkan Görsel', + 'table_column_title' => 'Başlık', + 'table_column_slug' => 'URL Yolu', + 'table_column_status' => 'Durum', + 'table_column_author' => 'Yazar', + 'table_column_published_at' => 'Yayın Tarihi', + 'table_column_parent' => 'Üst Sayfa', + 'table_column_template' => 'Şablon', + 'table_column_is_homepage' => 'Ana Sayfa', + 'table_column_show_in_menu' => 'Menüde Göster', + 'table_column_sort_order' => 'Sıralama', + 'table_column_created_at' => 'Oluşturulma Tarihi', + 'table_column_updated_at' => 'Güncellenme Tarihi', + + // Template options + 'template_default' => 'Varsayılan', + 'template_landing' => 'Landing Page', + 'template_blog' => 'Blog', + 'template_contact' => 'İletişim', + + // Copy message + 'copy_url_message' => 'URL kopyalandı', // Validation messages 'title_required' => 'Başlık alanı zorunludur.',