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.',