Refactor News management features: replace author text input with a searchable select component in NewsResource, update NewsPageController to fetch news with authors and recent articles, and enhance the news show view to display author details and comments more effectively.

This commit is contained in:
2025-07-28 21:05:24 +05:00
parent 1b4989b440
commit 6b0c92d838
12 changed files with 360 additions and 52 deletions

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Filament\Resources;
use App\Filament\Resources\AuthorResource\Pages;
use App\Filament\Resources\AuthorResource\RelationManagers;
use App\Models\Author;
use Filament\Forms;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class AuthorResource extends Resource
{
protected static ?string $model = Author::class;
protected static ?string $navigationIcon = 'heroicon-o-users';
protected static ?string $navigationGroup = 'News';
public static function form(Form $form):
Form
{
return $form
->schema([
Forms\Components\Card::make()
->schema([
TextInput::make('name')
->required()
->maxLength(255),
FileUpload::make('profile_image')
->label('Profile Image 400x400')
->image()
->directory('authors')
->nullable(),
RichEditor::make('description')
->nullable()
->columnSpanFull(),
])->columns(1),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
ImageColumn::make('profile_image')
->square()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('name')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
Tables\Columns\TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListAuthors::route('/'),
'create' => Pages\CreateAuthor::route('/create'),
'edit' => Pages\EditAuthor::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Filament\Resources\AuthorResource\Pages;
use App\Filament\Resources\AuthorResource;
use Filament\Actions;
use Filament\Resources\Pages\CreateRecord;
class CreateAuthor extends CreateRecord
{
protected static string $resource = AuthorResource::class;
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\AuthorResource\Pages;
use App\Filament\Resources\AuthorResource;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
class EditAuthor extends EditRecord
{
protected static string $resource = AuthorResource::class;
protected function getHeaderActions(): array
{
return [
Actions\DeleteAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\AuthorResource\Pages;
use App\Filament\Resources\AuthorResource;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
class ListAuthors extends ListRecords
{
protected static string $resource = AuthorResource::class;
protected function getHeaderActions(): array
{
return [
Actions\CreateAction::make(),
];
}
}

View File

@@ -9,6 +9,7 @@ use Filament\Forms;
use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\RichEditor; use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput; use Filament\Forms\Components\TextInput;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Resources\Resource; use Filament\Resources\Resource;
@@ -48,9 +49,11 @@ class NewsResource extends Resource
->directory('news') ->directory('news')
->nullable() ->nullable()
->columnSpanFull(), ->columnSpanFull(),
TextInput::make('author') Select::make('author_id')
->required() ->relationship('author', 'name')
->maxLength(255), ->searchable()
->preload()
->required(),
RichEditor::make('content') RichEditor::make('content')
->required() ->required()
->columnSpanFull(), ->columnSpanFull(),
@@ -75,7 +78,8 @@ class NewsResource extends Resource
Tables\Columns\TextColumn::make('slug') Tables\Columns\TextColumn::make('slug')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('author') Tables\Columns\TextColumn::make('author.name')
->label('Author Name')
->searchable() ->searchable()
->sortable(), ->sortable(),
Tables\Columns\TextColumn::make('published_at') Tables\Columns\TextColumn::make('published_at')

View File

@@ -9,14 +9,17 @@ class NewsPageController extends Controller
{ {
public function index() public function index()
{ {
$allNews = News::all(); $allNews = News::query()->with('author')->latest()->get();
return view('web.pages.news.index', compact('allNews')); return view('web.pages.news.index', compact('allNews'));
} }
public function show(News $news) public function show($news)
{ {
return view('web.pages.news.show', compact('news')); $news = News::where('slug', $news)->with('author', 'comments')->first();
$recentNews = News::query()->with('author')->latest()->limit(3)->get();
return view('web.pages.news.show', compact('news', 'recentNews'));
} }
public function storeComment(Request $request, News $news) public function storeComment(Request $request, News $news)

23
app/Models/Author.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Author extends Model
{
use HasFactory;
protected $fillable = [
'name',
'description',
'profile_image',
];
public function news(): HasMany
{
return $this->hasMany(News::class);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
class News extends Model class News extends Model
@@ -16,11 +17,16 @@ class News extends Model
'content', 'content',
'image', 'image',
'published_at', 'published_at',
'author', 'author_id',
]; ];
public function comments(): HasMany public function comments(): HasMany
{ {
return $this->hasMany(Comment::class); return $this->hasMany(Comment::class);
} }
public function author(): BelongsTo
{
return $this->belongsTo(Author::class);
}
} }

View File

@@ -0,0 +1,28 @@
<?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('news', function (Blueprint $table) {
$table->longText('author_description')->nullable()->after('author');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('news', function (Blueprint $table) {
$table->dropColumn('author_description');
});
}
};

View File

@@ -0,0 +1,30 @@
<?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('authors', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->longText('description')->nullable();
$table->string('profile_image')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('authors');
}
};

View File

@@ -0,0 +1,33 @@
<?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('news', function (Blueprint $table) {
$table->foreignId('author_id')->nullable()->constrained()->onDelete('set null');
$table->dropColumn('author');
$table->dropColumn('author_description');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('news', function (Blueprint $table) {
$table->dropForeign(['author_id']);
$table->dropColumn('author_id');
$table->string('author')->nullable();
$table->string('author_description')->nullable();
});
}
};

View File

@@ -20,68 +20,101 @@
</div> </div>
<!-- Breadcrumb Area End --> <!-- Breadcrumb Area End -->
<!-- Blog Single Area Start --> <!-- Blog Details Area Start -->
<div class="blog-single__area section-padding"> <div class="blog__details section-padding">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-xl-8 col-lg-8 lg-mb-50"> <div class="col-lg-8 lg-mb-25">
<div class="blog__single-left"> <div class="blog__details-area">
<div class="blog__single-left-image"> <img src="/storage/{{ $news->image }}" alt="image">
<img src="/storage/{{ $news->image }}" alt="{{ $news->title }}"> <h3 class="mt-25 mb-20">{{ $news->title }}</h3>
</div> <p>{{ $news->description }}</p>
<div class="blog__single-left-content">
<div class="blog__single-left-content-meta"> <div class="blog__details-area-author">
<ul> <div class="blog__details-area-author-image">
<li><i class="fa-regular fa-calendar"></i> {{ \Carbon\Carbon::parse($news->published_at)->format('d M, Y') }}</li> <img src="{{ $news->author->profile_image ?? '/web/assets/img/team/member-1.jpg' }}" alt="avatar">
<li><i class="fa-regular fa-user"></i> {{ $news->author }}</li> </div>
</ul> <div class="blog__details-area-author-content">
<h5>{{ $news->author->name ?? 'Admin' }}</h5>
<p>{{ $news->description ?? 'Super Admin' }}</p>
</div> </div>
<h3 class="mb-20">{{ $news->title }}</h3>
{!! $news->content !!}
</div> </div>
<!-- Comments Section --> <div class="blog__details-area-comment mt-40">
<div class="comments-area"> <h3 class="mb-30">Comments ({{ $news->comments->count() }})</h3>
<h3 class="comments-title">Comments ({{ $news->comments->count() }})</h3> @forelse ($news->comments as $comment)
<div class="comments-list"> <div class="blog__details-area-comment-item">
@foreach ($news->comments as $comment) <div class="blog__details-area-comment-item-comment">
<div class="single-comment-item"> <div class="blog__details-area-comment-item-comment-image">
<div class="single-comment-item-content"> <img src="/web/assets/img/team/member-5.jpg" alt="avatar-image">
<h5>{{ $comment->title }}</h5> </div>
<span>By {{ $comment->author_name ?? 'Anonymous' }} on {{ \Carbon\Carbon::parse($comment->created_at)->format('d M, Y') }}</span> <div class="blog__details-area-comment-item-comment-content">
<p>{{ $comment->message }}</p> <h5>{{ $comment->title ?? '-' }}</h5>
</div> <span>Submitted {{ $comment->created_at->diffForHumans() }} by {{ $comment->author_name ?? 'Anonymous' }}</span>
<p>{{ $comment->message }}</p>
</div> </div>
@endforeach </div>
</div> </div>
@empty
<div class="comment-form-area"> <p>No comments yet</p>
<h3 class="comment-form-title">Leave a Comment</h3> @endforelse
<form action="{{ route('comments.store', $news->slug) }}" method="POST"> </div>
<div class="blog__details-area-contact mt-60">
<h3>Post Comment</h3>
<p>Required fields are marked</p>
<div class="blog__details-area-contact-form">
<form action="{{ route('comments.store', $news->slug) }}" method="POST">
@csrf @csrf
<div class="row"> <div class="row">
<div class="col-lg-6 mb-25"> <div class="col-sm-6 mt-25">
<input type="text" name="title" placeholder="Comment Title*" required> <div class="blog__details-area-contact-form-item contact-item">
<input type="text" name="author_name" placeholder="Full Name" required="required">
</div>
</div> </div>
<div class="col-lg-6 mb-25"> <div class="col-sm-6 mt-25">
<input type="text" name="author_name" placeholder="Your Name (Optional)"> <div class="blog__details-area-contact-form-item contact-item">
<input type="text" name="title" placeholder="Title" required="required">
</div>
</div> </div>
<div class="col-lg-12 mb-25"> <div class="col-sm-12 mt-25">
<textarea name="message" rows="5" placeholder="Your Message*" required></textarea> <div class="blog__details-area-contact-form-item contact-item">
<textarea name="message" placeholder="Type your comments...."></textarea>
<input type="hidden" name="news_id" value="{{ $news->id }}">
</div>
</div> </div>
<div class="col-lg-12"> <div class="col-lg-12 mt-25">
<button type="submit" class="theme-btn">Post Comment<i class="flaticon-right-up"></i></button> <div class="blog__details-area-contact-form-item">
<button class="build_button" type="submit">Submit Comment<i class="flaticon-right-up"></i></button>
</div>
</div> </div>
</div> </div>
</form> </form>
</div>
</div>
</div>
</div>
<div class="col-lg-4 columns_sticky">
<div class="all__sidebar">
<div class="all__sidebar-item">
<h4>Recent Blog</h4>
<div class="all__sidebar-item-post dark_image">
@foreach($recentNews as $news)
<div class="post__item">
<div class="post__item-image">
<a href="{{ route('news.show', $news->slug) }}"><img src="/storage/{{ $news->image }}" alt="post-image"></a>
</div>
<div class="post__item-title">
<h6><a href="{{ route('news.show', $news->slug) }}">{{ $news->title }}</a></h6>
<span><i class="far fa-calendar-alt"></i>{{ \Carbon\Carbon::parse($news->published_at)->format('d M Y') }}</span>
</div>
</div>
@endforeach
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-xl-4 col-lg-4">
<!-- You can add a sidebar here if needed -->
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Blog Single Area End --> <!-- Blog Details Area End -->
@endsection @endsection