feat: Add Livewire-powered news section with dedicated routes and centralize header/footer in the app layout.

This commit is contained in:
Yoga Pangestu 2026-01-05 14:59:18 +07:00
parent 5b4efc8a5d
commit 4bfa39d14e
8 changed files with 469 additions and 3 deletions

View File

@ -0,0 +1,97 @@
<?php
namespace App\Livewire\Home\News;
use App\Models\Category;
use App\Models\News;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Spatie\Tags\Tag;
#[Title('Berita')]
class Index extends Component
{
use WithPagination;
#[Url(as: 'q')]
public $search = '';
#[Url(as: 'category')]
public $category = '';
#[Url(as: 'tag')]
public $tag = '';
public function updatedSearch()
{
$this->resetPage();
}
public function updatedCategory()
{
$this->resetPage();
}
public function updatedTag()
{
$this->resetPage();
}
public function render()
{
$news = News::published()
->with(['categories', 'author'])
->when($this->search, function ($query) {
$query->where('title', 'like', '%'.$this->search.'%')
->orWhere('content', 'like', '%'.$this->search.'%');
})
->when($this->category, function ($query) {
$query->whereHas('categories', function ($query) {
$query->where('categories.id', $this->category);
});
})
->when($this->tag, function ($query) {
$query->withAnyTags([$this->tag]);
})
->latest('published_at')
->paginate(5)
->through(function ($news) {
$news->thumbnail = $news->getFirstMediaUrl('news') ?: 'https://images.pexels.com/photos/12199409/pexels-photo-12199409.jpeg';
$news->formatted_date = $news->published_at?->translatedFormat('d F Y') ?: '-';
$news->author_name = $news->author?->name ?: 'Admin';
$news->formatted_views = number_format($news->views).' Views';
$news->excerpt = str($news->content)->stripTags()->limit(160);
return $news;
});
$categories = Category::active()
->withCount(['news' => function ($query) {
$query->published();
}])
->orderBy('sort_order')
->get();
$popularPosts = News::published()
->latest('views')
->limit(3)
->get()
->map(function ($news) {
$news->thumbnail = $news->getFirstMediaUrl('news') ?: 'https://images.pexels.com/photos/12199409/pexels-photo-12199409.jpeg';
$news->formatted_date = $news->published_at?->translatedFormat('d F Y') ?: '-';
return $news;
});
return view('livewire.home.news.index', [
'pageTitle' => 'Berita',
'pageDescription' => 'Jelajahi berita terbaru seputar purwakarta.',
'news' => $news,
'popularPosts' => $popularPosts,
'categories' => $categories,
'tags' => Tag::all(),
]);
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace App\Livewire\Home\News;
use App\Enums\NewsStatus;
use App\Models\News;
use Livewire\Attributes\Title;
use Livewire\Component;
class Show extends Component
{
public News $news;
public function mount(News $news)
{
if ($news->status !== NewsStatus::PUBLISHED) {
abort(404);
}
$this->news = $news;
$this->news->increment('views');
}
#[Title('Detail Berita')]
public function render()
{
$this->news->load(['categories', 'author', 'tags']);
// Format main news
$this->news->thumbnail = $this->news->getFirstMediaUrl('news') ?: 'https://images.pexels.com/photos/12199409/pexels-photo-12199409.jpeg';
$this->news->formatted_date = $this->news->published_at?->translatedFormat('d F Y') ?: '-';
$this->news->author_name = $this->news->author?->name ?: 'Admin';
$this->news->author_avatar = 'https://ui-avatars.com/api/?name='.urlencode($this->news->author?->name ?: 'A');
$this->news->formatted_views = number_format($this->news->views).' Views';
$relatedNews = News::published()
->where('id', '!=', $this->news->id)
->whereHas('categories', function ($query) {
$query->whereIn('categories.id', $this->news->categories->pluck('id'));
})
->latest('published_at')
->limit(4)
->get();
if ($relatedNews->isEmpty()) {
$relatedNews = News::published()
->where('id', '!=', $this->news->id)
->latest('published_at')
->limit(4)
->get();
}
// Format related news
$relatedNews = $relatedNews->map(function ($news) {
$news->thumbnail = $news->getFirstMediaUrl('news') ?: 'https://images.pexels.com/photos/12199409/pexels-photo-12199409.jpeg';
$news->formatted_date = $news->published_at?->translatedFormat('d F Y') ?: '-';
$news->primary_category = $news->categories->first();
return $news;
});
return view('livewire.home.news.show', [
'relatedNews' => $relatedNews,
]);
}
}

View File

@ -27,8 +27,12 @@
</head>
<body data-mobile-nav-style="full-screen-menu" data-mobile-nav-bg-color="#252840">
<x-partials.header />
{{ $slot }}
<x-partials.footer />
<script type="text/javascript" src="{{ asset('js/home/jquery.js') }}"></script>
<script type="text/javascript" src="{{ asset('js/home/vendors.js') }}"></script>
<script type="text/javascript" src="{{ asset('js/home/main.js') }}"></script>

View File

@ -38,7 +38,9 @@ class="mobile-logo">
<a href="{{ route('home') }}"
class="nav-link {{ Route::is('home') ? 'active' : '' }}">Beranda</a>
</li>
<li class="nav-item"><a href="demo-it-business-about.html" class="nav-link">Berita</a>
<li class="nav-item">
<a wire:navigate href="{{ route('news.index') }}"
class="nav-link {{ Route::is('news.index') || Route::is('news.show') ? 'active' : '' }}">Berita</a>
</li>
<li class="nav-item"><a href="demo-it-business-about.html" class="nav-link">Pengumuman</a>
</li>

View File

@ -1,5 +1,4 @@
<div>
<x-partials.header />
<!-- start hero section -->
<section class="cover-background full-screen ipad-top-space-margin py-0 md-h-750px sm-h-650px"
style="
@ -1046,7 +1045,6 @@ class="btn btn-extra-large btn-switch-text btn-gradient-purple-pink btn-rounded
</div>
</section>
<!-- end section -->
<x-partials.footer />
<!-- start scroll progress -->
<div class="scroll-progress d-none d-xxl-block">
<a href="#" class="scroll-top" aria-label="scroll">

View File

@ -0,0 +1,169 @@
<div>
<section class="pt-0 cover-background ipad-top-space-margin sm-pb-0"
style="background-image:
linear-gradient(rgba(0,0,0,0.45), rgba(0,0,0,0.45)),
url('https://images.pexels.com/photos/12199409/pexels-photo-12199409.jpeg');">
<div class="shape-image-animation bottom-0 p-0 w-100 d-none d-md-block">
<svg xmlns="http://www.w3.org/2000/svg" widht="3000" height="400" viewBox="0 180 2500 200" fill="#ffffff">
<path class="st1" d="M 0 250 C 1200 400 1200 50 3000 250 L 3000 550 L 0 550 L 0 250">
<animate attributeName="d" dur="5s"
values="M 0 250 C 1200 400 1200 50 3000 250 L 3000 550 L 0 550 L 0 250;
M 0 250 C 400 50 400 400 3000 250 L 3000 550 L 0 550 L 0 250;
M 0 250 C 1200 400 1200 50 3000 250 L 3000 550 L 0 550 L 0 250"
repeatCount="indefinite" />
</path>
</svg>
</div>
<div class="container">
<div class="row align-items-center justify-content-center h-500px sm-h-300px">
<div class="col-12 col-lg-6 col-md-10 position-relative text-center page-title-extra-large d-flex flex-wrap flex-column align-items-center justify-content-center"
data-anime='{ "el": "childs", "translateY": [30, 0], "opacity": [0,1], "duration": 600, "delay": 0, "staggervalue": 300, "easing": "easeOutQuad" }'>
<span
class="ps-25px pe-25px pt-5px pb-5px mb-15px text-uppercase text-white fs-12 ls-1px fw-600 border-radius-100px bg-gradient-dark-gray-transparent d-flex"><i
class="bi bi-megaphone text-white icon-small me-10px"></i>{{ $pageTitle }}</span>
<h5 class="mb-20px text-white fw-600 ls-minus-1px">{{ $pageDescription }}</h5>
</div>
</div>
</div>
</section>
<section class="pt-0 right-side-bar">
<div class="container">
<div class="row justify-content-center">
<div class="col-12 col-lg-8 blog-standard md-mb-50px sm-mb-40px">
@forelse($news as $item)
<!-- start blog item -->
<div class="col-12 mb-40px">
<div class="card border-0 no-border-radius box-shadow-extra-large">
@if ($item->thumbnail)
<div class="blog-image">
<a href="{{ route('news.show', $item->slug) }}">
<img src="{{ $item->thumbnail }}" alt="{{ $item->title }}"
class="w-100" />
</a>
<div class="blog-categories">
@foreach ($item->categories as $itemCategory)
<a href="javascript:void(0)"
wire:click="$set('category', '{{ $itemCategory->id }}')"
class="categories-btn bg-white text-dark-gray text-dark-gray-hover text-uppercase alt-font fw-600">{{ $itemCategory->name }}</a>
@endforeach
</div>
</div>
@endif
<div class="card-body p-9 bg-white">
<div class="entry-meta mb-20px fs-15">
<span><i class="feather icon-feather-calendar"></i><a
href="javascript:void(0)">{{ $item->formatted_date }}</a></span>
<span><i class="feather icon-feather-user"></i><a
href="javascript:void(0)">{{ $item->author_name }}</a></span>
<span><i class="feather icon-feather-eye"></i><a
href="javascript:void(0)">{{ $item->formatted_views }}</a></span>
</div>
<a href="{{ route('news.show', $item->slug) }}"
class="text-dark-gray card-title mb-20px fw-600 fs-24 d-block">{{ $item->title }}</a>
<p class="text-medium-gray mb-20px">
{{ $item->excerpt }}
</p>
<a href="{{ route('news.show', $item->slug) }}"
class="btn btn-link btn-large text-base-color fw-600">Teruskan Membaca<span
class="bg-base-color"></span></a>
</div>
</div>
</div>
<!-- end blog item -->
@empty
<div class="col-12 text-center">
<p>Tidak ada berita ditemukan.</p>
</div>
@endforelse
<div class="col-12 mt-8 d-flex justify-content-center">
{{ $news->links() }}
</div>
</div>
<!-- start sidebar -->
<aside class="col-12 col-xl-4 col-lg-4 col-md-7 ps-55px xl-ps-50px lg-ps-15px sidebar">
<div class="mb-15 md-mb-50px xs-mb-35px">
<div
class="fw-600 fs-19 lh-22 ls-minus-05px text-dark-gray border-bottom border-color-dark-gray border-2 d-block mb-30px pb-15px position-relative">
Cari</div>
<div class="position-relative">
<input class="form-control" type="text" wire:model.live.debounce.300ms="search"
placeholder="Cari berita...">
<button class="bg-transparent border-0 position-absolute right-15px top-10px"
type="submit"><i class="feather icon-feather-search"></i></button>
</div>
</div>
<div class="mb-15 md-mb-50px xs-mb-35px">
<div
class="fw-600 fs-19 lh-22 ls-minus-05px text-dark-gray border-bottom border-color-dark-gray border-2 d-block mb-30px pb-15px position-relative">
Berita Terpopuler</div>
<ul class="popular-post-sidebar position-relative">
@foreach ($popularPosts as $post)
<li class="d-flex align-items-start">
@if ($post->thumbnail)
<figure>
<a href="{{ route('news.show', $post->slug) }}">
<img src="{{ $post->thumbnail }}" alt="image" class="w-100">
</a>
</figure>
@endif
<div class="col media-body">
<a href="{{ route('news.show', $post->slug) }}"
class="fw-600 fs-17 text-dark-gray d-inline-block mb-10px">{{ $post->title }}</a>
<div><a href="javascript:void(0)"
class="d-inline-block fs-15">{{ $post->formatted_date }}</a>
</div>
</div>
</li>
@endforeach
</ul>
</div>
<div class="mb-15 md-mb-50px xs-mb-35px">
<div
class="fw-600 fs-19 lh-22 ls-minus-05px text-dark-gray border-bottom border-color-dark-gray border-2 d-block mb-30px pb-15px position-relative">
Kategori</div>
<ul class="category-list-sidebar position-relative list-unstyled">
<li class="mb-10px">
<a href="javascript:void(0)" wire:click="$set('category', '')"
class="d-flex align-items-center {{ $this->category == '' ? 'text-base-color fw-600' : 'text-dark-gray' }}">
<span>Semua Kategori</span>
</a>
</li>
@foreach ($categories as $cat)
<li class="mb-10px border-bottom border-color-extra-light-gray pb-10px">
<a href="javascript:void(0)" wire:click="$set('category', '{{ $cat->id }}')"
class="d-flex align-items-center justify-content-between {{ $this->category == $cat->id ? 'text-base-color fw-600' : 'text-dark-gray' }}">
<span>{{ $cat->name }}</span>
<span class="fs-14 text-medium-gray">({{ $cat->news_count }})</span>
</a>
</li>
@endforeach
</ul>
</div>
@if ($tags->count() > 0)
<div class="mb-15 md-mb-50px xs-mb-35px">
<div
class="fw-600 fs-19 lh-22 ls-minus-05px text-dark-gray border-bottom border-color-dark-gray border-2 d-block mb-30px pb-15px position-relative">
Tags</div>
<div class="tag-cloud">
@foreach ($tags as $item)
<a href="javascript:void(0)" wire:click="$set('tag', '{{ $item->name }}')"
class="{{ $tag == $item->name ? 'bg-base-color text-white' : '' }}">{{ $item->name }}</a>
@endforeach
@if ($tag)
<a href="javascript:void(0)" wire:click="$set('tag', '')"
class="bg-dark-gray text-white">Reset Tag</a>
@endif
</div>
</div>
@endif
</aside>
<!-- end sidebar -->
</div>
</div>
</section>
</div>

View File

@ -0,0 +1,123 @@
<div>
<!-- start section -->
<section class="one-fourth-screen cover-background bg-dark-gray ipad-top-space-margin sm-mb-50px"
data-parallax-background-ratio="0.5" style="background-image: url({{ $news->thumbnail }})">
</section>
<!-- end section -->
<!-- start section -->
<section class="p-0">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10 overlap-section text-center">
<div class="p-10 box-shadow-extra-large border-radius-4px bg-white text-center">
@foreach ($news->categories as $category)
<a href="{{ route('news.index', ['category' => $category->id]) }}"
class="bg-solitude-blue text-uppercase fs-13 ps-25px pe-25px alt-font fw-500 text-base-color lh-40 sm-lh-55 border-radius-100px d-inline-block mb-3 sm-mb-15px">{{ $category->name }}</a>
@endforeach
<h3 class="alt-font text-dark-gray fw-600 ls-minus-1px mb-15px">{{ $news->title }}</h3>
<div class="lg-20px sm-mb-0">
<span>{{ $news->author_name }}</span>
<span class="ms-10px">{{ $news->formatted_date }}</span>
<span class="ms-10px"><i class="feather icon-feather-eye"></i>
{{ $news->formatted_views }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- end section -->
<!-- start section -->
<section class="overlap-section text-center p-0 sm-pt-30px">
<img class="rounded-circle box-shadow-extra-large w-130px h-130px border border-8 border-color-white"
src="{{ $news->author_avatar }}" alt="{{ $news->author_name }}">
</section>
<!-- end section -->
<!-- start section -->
<section class="pb-0 pt-3">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-9 last-paragraph-no-margin"
data-anime='{ "el": "childs", "translateY": [50, 0], "opacity": [0,1], "duration": 1200, "delay": 0, "staggervalue": 150, "easing": "easeOutQuad" }'>
<div class="text-medium-gray entry-content">
{!! $news->content !!}
</div>
</div>
</div>
</div>
</section>
<!-- end section -->
<!-- start section -->
<section class="half-section pb-0">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-9"
data-anime='{ "el": "childs", "translateY": [50, 0], "opacity": [0,1], "duration": 1200, "delay": 0, "staggervalue": 150, "easing": "easeOutQuad" }'>
<div class="row mb-30px">
<div class="tag-cloud col-md-12 text-center text-md-start sm-mb-15px">
@foreach ($news->tags as $tag)
<a href="{{ route('news.index', ['tag' => $tag->name]) }}">{{ $tag->name }}</a>
@endforeach
</div>
</div>
</div>
</div>
</div>
</section>
<!-- end section -->
<!-- start section -->
<section class="bg-solitude-blue position-relative sm-pb-20px mt-5">
<div class="container">
<div class="row justify-content-center mb-1">
<div class="col-lg-7 text-center">
<span
class="bg-white box-shadow-quadruple-large text-uppercase fs-13 ps-25px pe-25px alt-font fw-600 text-base-color lh-40 sm-lh-55 border-radius-100px d-inline-block mb-25px"
data-anime='{ "translateY": [30, 0], "opacity": [0,1], "delay": 500, "staggervalue": 100, "easing": "easeOutQuad" }'>Mungkin
Anda suka</span>
<h3 class="alt-font text-dark-gray fw-600 ls-minus-1px"
data-anime='{ "el": "lines", "translateY": [30, 0], "opacity": [0,1], "delay": 500, "staggervalue": 100, "easing": "easeOutQuad" }'>
Berita Terkait</h3>
</div>
</div>
<div class="row">
<div class="col-12 px-0">
<ul class="blog-classic blog-wrapper grid grid-4col xl-grid-4col lg-grid-3col md-grid-2col sm-grid-2col xs-grid-1col gutter-double-extra-large"
data-anime='{ "el": "childs", "translateY": [50, 0], "opacity": [0,1], "duration": 600, "delay": 0, "staggervalue": 300, "easing": "easeOutQuad" }'>
<li class="grid-sizer"></li>
@foreach ($relatedNews as $related)
<!-- start blog item -->
<li class="grid-item">
<div class="card bg-transparent border-0 h-100">
<div class="blog-image position-relative overflow-hidden border-radius-4px">
<a href="{{ route('news.show', $related->slug) }}"><img
src="{{ $related->thumbnail }}" alt="{{ $related->title }}"
class="w-100" /></a>
</div>
<div class="card-body px-0 pb-30px pt-30px xs-pb-15px last-paragraph-no-margin">
<span class="fs-13 text-uppercase mb-5px d-block">
@if ($related->primary_category)
<a href="{{ route('news.index', ['category' => $related->primary_category->id]) }}"
class="text-dark-gray fw-500 categories-text">{{ $related->primary_category->name }}</a>
@endif
<a href="javascript:void(0)"
class="blog-date">{{ $related->formatted_date }}</a>
</span>
<a href="{{ route('news.show', $related->slug) }}"
class="card-title mb-0 fw-500 fs-18 lh-30 text-dark-gray d-inline-block">{{ $related->title }}</a>
</div>
</div>
</li>
<!-- end blog item -->
@endforeach
</ul>
</div>
</div>
</div>
</section>
<!-- end section -->
</div>

View File

@ -1,6 +1,13 @@
<?php
use App\Livewire\Home\Index;
use App\Livewire\Home\News\Index as NewsIndex;
use App\Livewire\Home\News\Show as NewsShow;
use Illuminate\Support\Facades\Route;
Route::get('/', Index::class)->name('home');
Route::prefix('news')->group(function () {
Route::get('/', NewsIndex::class)->name('news.index');
Route::get('/{news:slug}', NewsShow::class)->name('news.show');
});