diff --git a/app/Filament/Resources/Publication/News/NewsResource.php b/app/Filament/Resources/Publication/News/NewsResource.php new file mode 100644 index 0000000..27610fb --- /dev/null +++ b/app/Filament/Resources/Publication/News/NewsResource.php @@ -0,0 +1,69 @@ + ListNews::route('/'), + 'create' => CreateNews::route('/create'), + 'edit' => EditNews::route('/{record}/edit'), + ]; + } + + public static function getRecordRouteBindingEloquentQuery(): Builder + { + return parent::getRecordRouteBindingEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Resources/Publication/News/Pages/CreateNews.php b/app/Filament/Resources/Publication/News/Pages/CreateNews.php new file mode 100644 index 0000000..76ae6ff --- /dev/null +++ b/app/Filament/Resources/Publication/News/Pages/CreateNews.php @@ -0,0 +1,38 @@ +getResource()::getUrl('index'); + } + + protected function getHeaderActions(): array + { + return [ + Action::make('back') + ->label('Kembali') + ->url(fn (): string => route('filament.dashboard.resources.publication.news.index')) + ->outlined() + ->color('secondary'), + ]; + } + + protected function handleRecordCreation(array $data): Model + { + $data['author_id'] = auth()->id(); + + return static::getModel()::create($data); + } +} diff --git a/app/Filament/Resources/Publication/News/Pages/EditNews.php b/app/Filament/Resources/Publication/News/Pages/EditNews.php new file mode 100644 index 0000000..a27695e --- /dev/null +++ b/app/Filament/Resources/Publication/News/Pages/EditNews.php @@ -0,0 +1,28 @@ +label('Kembali') + ->url(fn (): string => route('filament.dashboard.resources.publication.news.index')) + ->outlined() + ->color('secondary'), + ]; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } +} diff --git a/app/Filament/Resources/Publication/News/Pages/ListNews.php b/app/Filament/Resources/Publication/News/Pages/ListNews.php new file mode 100644 index 0000000..0c6e2fe --- /dev/null +++ b/app/Filament/Resources/Publication/News/Pages/ListNews.php @@ -0,0 +1,22 @@ +label('Tambah'), + ]; + } +} diff --git a/app/Filament/Resources/Publication/News/Schemas/NewsForm.php b/app/Filament/Resources/Publication/News/Schemas/NewsForm.php new file mode 100644 index 0000000..158ebc8 --- /dev/null +++ b/app/Filament/Resources/Publication/News/Schemas/NewsForm.php @@ -0,0 +1,66 @@ +components([ + Section::make([ + TextInput::make('title') + ->label('Judul') + ->placeholder('Berita Purwakarta Hari Ini') + ->autocomplete(false) + ->autofocus() + ->required() + ->maxLength(200), + + RichEditor::make('content') + ->label('Konten') + ->placeholder('...') + ->required() + ->columnSpanFull(), + + TextInput::make('link') + ->label('Tautan') + ->placeholder('https://www.pwknews.com') + ->autocomplete(false) + ->nullable() + ->maxLength(50) + ->url(), + ])->columnSpan(2), + + Section::make([ + TagsInput::make('tags') + ->reactive() + ->afterStateHydrated(function ($component, $state, $record) { + if (! $record) { + return; + } + $component->state( + $record->tags->pluck('name')->toArray() + ); + }), + + SpatieMediaLibraryFileUpload::make('thumbnail') + ->label('Thumbnail') + ->disk(config('filesystems.default')) + ->acceptedFileTypes(['images/*']) + ->maxSize(1024 * 3) + ->collection('news') + ->image() + ->required(), + ]), + ]) + ->columns(3); + } +} diff --git a/app/Filament/Resources/Publication/News/Tables/NewsTable.php b/app/Filament/Resources/Publication/News/Tables/NewsTable.php new file mode 100644 index 0000000..f02f477 --- /dev/null +++ b/app/Filament/Resources/Publication/News/Tables/NewsTable.php @@ -0,0 +1,78 @@ +columns([ + TextColumn::make('title') + ->label('Judul') + ->searchable() + ->sortable() + ->wrap() + ->limit(50), + + TextColumn::make('author.name') + ->label('Penulis') + ->searchable() + ->sortable() + ->placeholder('Tidak ada'), + + TextColumn::make('view') + ->label('Dilihat') + ->numeric() + ->sortable() + ->alignCenter(), + + TextColumn::make('tags.name') + ->label('Tag') + ->listWithLineBreaks() + ->limitList(3) + ->expandableLimitedList() + ->badge() + ->placeholder('Tidak ada tag'), + + TextColumn::make('link') + ->label('Tautan') + ->searchable() + ->sortable() + ->copyable() + ->copyMessage('Tautan berhasil disalin') + ->placeholder('Tidak ada'), + + ...TimestampColumns::make(), + ]) + ->filters([ + TrashedFilter::make()->native(false), + ]) + ->recordActions([ + EditAction::make(), + DeleteAction::make(), + ForceDeleteAction::make(), + RestoreAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + ...DefaultBulkActions::make('Berita'), + ]), + ]) + ->emptyStateIcon('heroicon-o-newspaper') + ->emptyStateDescription('Setelah Anda membuat data pertama, maka akan muncul disini.') + ->defaultSort('created_at', 'desc') + ->deferFilters(false); + } +} diff --git a/app/Models/News.php b/app/Models/News.php new file mode 100644 index 0000000..cd392ec --- /dev/null +++ b/app/Models/News.php @@ -0,0 +1,32 @@ +generateSlugsFrom('title') + ->saveSlugsTo('slug'); + } + + public function author(): BelongsTo + { + return $this->belongsTo(User::class, 'author_id'); + } +} diff --git a/database/factories/NewsFactory.php b/database/factories/NewsFactory.php new file mode 100644 index 0000000..c57efce --- /dev/null +++ b/database/factories/NewsFactory.php @@ -0,0 +1,65 @@ + + */ +class NewsFactory extends Factory +{ + protected $model = News::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $title = $this->faker->sentence(6); + + return [ + 'author_id' => User::factory(), + 'title' => $title, + 'slug' => Str::slug($title), + 'content' => $this->faker->paragraphs(3, true), + 'link' => $this->faker->optional(0.3)->url(), + 'view' => $this->faker->numberBetween(0, 10000), + ]; + } + + /** + * Indicate that the news has high views. + */ + public function popular(): static + { + return $this->state(fn (array $attributes) => [ + 'view' => $this->faker->numberBetween(5000, 50000), + ]); + } + + /** + * Indicate that the news has external link. + */ + public function withLink(): static + { + return $this->state(fn (array $attributes) => [ + 'link' => $this->faker->url(), + ]); + } + + /** + * Indicate that the news has no views. + */ + public function unpublished(): static + { + return $this->state(fn (array $attributes) => [ + 'view' => 0, + ]); + } +} diff --git a/database/factories/NewsTagsFactory.php b/database/factories/NewsTagsFactory.php new file mode 100644 index 0000000..509db56 --- /dev/null +++ b/database/factories/NewsTagsFactory.php @@ -0,0 +1,81 @@ + + */ +class NewsTagsFactory extends Factory +{ + protected $model = NewsTags::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + $tags = [ + 'Berita', + 'Update', + 'Pengumuman', + 'Event', + 'Kegiatan', + 'Kominfo', + 'Diskominfo', + 'Pemerintah', + 'Publik', + 'Informasi', + 'Layanan', + 'Digital', + 'Teknologi', + 'Sosial', + 'Ekonomi', + 'Pendidikan', + 'Kesehatan', + 'Lingkungan', + 'Infrastruktur', + 'Transportasi', + ]; + + return [ + 'news_id' => News::factory(), + 'name' => $this->faker->randomElement($tags), + ]; + } + + /** + * Indicate that the tag is for news category. + */ + public function news(): static + { + return $this->state(fn (array $attributes) => [ + 'name' => 'Berita', + ]); + } + + /** + * Indicate that the tag is for announcement category. + */ + public function announcement(): static + { + return $this->state(fn (array $attributes) => [ + 'name' => 'Pengumuman', + ]); + } + + /** + * Indicate that the tag is for event category. + */ + public function event(): static + { + return $this->state(fn (array $attributes) => [ + 'name' => 'Event', + ]); + } +} diff --git a/database/migrations/2025_12_12_082036_create_news_table.php b/database/migrations/2025_12_12_082036_create_news_table.php new file mode 100644 index 0000000..45b52a1 --- /dev/null +++ b/database/migrations/2025_12_12_082036_create_news_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignIdFor(User::class, 'author_id'); + $table->string('title', 200); + $table->string('slug', 200); + $table->text('content'); + $table->string('link', 50)->nullable(); + $table->unsignedInteger('view')->default(0); + $table->timestamp('created_at')->useCurrent(); + $table->timestamp('updated_at')->nullable()->useCurrentOnUpdate(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('news'); + } +}; diff --git a/database/migrations/2025_12_12_085015_create_tag_tables.php b/database/migrations/2025_12_12_085015_create_tag_tables.php new file mode 100644 index 0000000..5925c6c --- /dev/null +++ b/database/migrations/2025_12_12_085015_create_tag_tables.php @@ -0,0 +1,36 @@ +id(); + + $table->json('name'); + $table->json('slug'); + $table->string('type')->nullable(); + $table->integer('order_column')->nullable(); + + $table->timestamps(); + }); + + Schema::create('taggables', function (Blueprint $table) { + $table->foreignId('tag_id')->constrained()->cascadeOnDelete(); + + $table->morphs('taggable'); + + $table->unique(['tag_id', 'taggable_id', 'taggable_type']); + }); + } + + public function down(): void + { + Schema::dropIfExists('taggables'); + Schema::dropIfExists('tags'); + } +};