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.

This commit is contained in:
Ümit Tunç
2025-09-29 10:20:40 -03:00
parent a2087e7178
commit 79a1f5b746
20 changed files with 1145 additions and 70 deletions
+11
View File
@@ -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
@@ -0,0 +1,73 @@
<?php
namespace App\Filament\Admin\Resources\Blogs;
use App\Filament\Admin\Resources\Blogs\Pages\CreateBlog;
use App\Filament\Admin\Resources\Blogs\Pages\EditBlog;
use App\Filament\Admin\Resources\Blogs\Pages\ListBlogs;
use App\Filament\Admin\Resources\Blogs\Schemas\BlogForm;
use App\Filament\Admin\Resources\Blogs\Tables\BlogsTable;
use App\Models\Blog;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class BlogResource extends Resource
{
protected static ?string $model = Blog::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedDocumentText;
public static function getNavigationLabel(): string
{
return __('blog.navigation_label');
}
public static function getModelLabel(): string
{
return __('blog.model_label');
}
public static function getPluralModelLabel(): string
{
return __('blog.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return BlogForm::configure($schema);
}
public static function table(Table $table): Table
{
return BlogsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListBlogs::route('/'),
'create' => CreateBlog::route('/create'),
'edit' => EditBlog::route('/{record}/edit'),
];
}
public static function getRecordRouteBindingEloquentQuery(): Builder
{
return parent::getRecordRouteBindingEloquentQuery()
->withoutGlobalScopes([
SoftDeletingScope::class,
]);
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Filament\Admin\Resources\Blogs\Pages;
use App\Filament\Admin\Resources\Blogs\BlogResource;
use Filament\Resources\Pages\CreateRecord;
class CreateBlog extends CreateRecord
{
protected static string $resource = BlogResource::class;
public function getTitle(): string
{
return __('blog.create');
}
protected function getRedirectUrl(): string
{
return $this->getResource()::getUrl('index');
}
protected function getCreatedNotificationTitle(): ?string
{
return __('blog.created_successfully');
}
}
@@ -0,0 +1,41 @@
<?php
namespace App\Filament\Admin\Resources\Blogs\Pages;
use App\Filament\Admin\Resources\Blogs\BlogResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditBlog extends EditRecord
{
protected static string $resource = BlogResource::class;
public function getTitle(): string
{
return __('blog.edit');
}
protected function getHeaderActions(): array
{
return [
DeleteAction::make()
->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');
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Filament\Admin\Resources\Blogs\Pages;
use App\Filament\Admin\Resources\Blogs\BlogResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListBlogs extends ListRecords
{
protected static string $resource = BlogResource::class;
public function getTitle(): string
{
return __('blog.title');
}
protected function getHeaderActions(): array
{
return [
CreateAction::make()
->label(__('blog.create')),
];
}
}
@@ -0,0 +1,154 @@
<?php
namespace App\Filament\Admin\Resources\Blogs\Schemas;
use Filament\Forms\Components\Checkbox;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\RichEditor;
use Filament\Schemas\Components\Section;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class BlogForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->columns(3)
->schema([
// Sol kolon - Ana içerik (2 sütun genişliğinde)
Section::make(__('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),
]);
}
}
@@ -0,0 +1,126 @@
<?php
namespace App\Filament\Admin\Resources\Blogs\Tables;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
class BlogsTable
{
public static function configure(Table $table): Table
{
return $table
->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');
}
}
@@ -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)
@@ -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(),
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Blog extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'title',
'slug',
'content',
'excerpt',
'meta_title',
'meta_description',
'status',
'featured_image',
'published_at',
'author_id',
'category_id',
'tags',
'view_count',
'is_featured',
'allow_comments',
];
protected $casts = [
'published_at' => '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);
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class BlogCategory extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'name',
'slug',
'description',
'color',
'sort_order',
'is_active',
];
protected $casts = [
'sort_order' => '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');
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class BlogComment extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'blog_id',
'name',
'email',
'content',
'status',
'parent_id',
'ip_address',
'user_agent',
];
protected $casts = [
'parent_id' => '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');
}
}
@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('blog_categories', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('blogs', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('blog_comments', function (Blueprint $table) {
$table->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');
}
};
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('blogs', function (Blueprint $table) {
// Eğer deleted_at sütunu yoksa ekle
if (!Schema::hasColumn('blogs', 'deleted_at')) {
$table->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();
}
});
}
};
+78
View File
@@ -0,0 +1,78 @@
<?php
return [
'title' => '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.',
];
+56 -5
View File
@@ -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.',
+78
View File
@@ -0,0 +1,78 @@
<?php
return [
'title' => '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.',
];
+56 -5
View File
@@ -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.',