Hello.
I'm a bit stuck, could you help me? I have created a product list with categories.
How it works:
You select a category from the list on the left, and the corresponding products are loaded on the right (the category slug is also added to the URL). If you navigate to a product page, the product's slug is also included in the URL. From the product page, you should be able to go back to the product list.
My problem:
If I try to go back to the product list from a product page that belongs to a category with subcategories, I get a 404 error.
If I navigate back from this URL: localhost/products/category_name/sub_category_name/product_name
to this URL: localhost/products/category_name/sub_category_name
The code works fine when trying to go back to the product list from a product page that belongs to a category without subcategories.
If I navigate back from this URL: localhost/products/category_name/product_name
to this URL: localhost/products/category_name
Here is my code:
app\Livewire\ProductPage.php
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Category;
use App\Models\Product;
use Illuminate\Support\Facades\Log;
class ProductPage extends Component
{
public $categories;
public $products = [];
public $selectedCategoryId = null;
public $openCategories = [];
public $categorySlugPaths = [];
public function mount($categorySlugPath = null)
{
$this->categories = Category::whereNull('parent_id')->with(['children', 'products'])->get();
if ($categorySlugPath) {
$slugs = explode('/', $categorySlugPath);
$category = $this->findCategoryBySlugs($slugs);
if ($category) {
Log::info("✅ Megtalált kategória: " . $category->name);
$this->selectCategory($category->id);
} else {
Log::error("❌ HIBA: Kategória nem található - " . $categorySlugPath);
abort(404);
}
}
}
public function selectCategory($categoryId)
{
$category = Category::find($categoryId);
if ($category) {
$this->selectedCategoryId = $categoryId;
$this->products = Product::where('category_id', $categoryId)->get();
$slugPath = $this->buildCategorySlugPath($category);
Log::info("🔄 Új URL beállítása: " . $slugPath);
if (!empty($slugPath)) {
$url = route('products.category', ['categorySlugPath' => $slugPath]);
Log::info("🔄 Livewire állapot frissítése URL-re: " . $url);
$this->dispatch('replaceState', url: $url);
}
foreach ($this->products as $product) {
$this->categorySlugPaths[$product->id] = $slugPath;
}
$this->openCategories = [];
while ($category && $category->parent_id) {
$this->openCategories[$category->parent_id] = true;
$category = Category::find($category->parent_id);
}
} else {
Log::error("❌ HIBA: A kiválasztott kategória nem található: " . $categoryId);
}
}
private function findCategoryBySlugs($slugs)
{
Log::info("🔎 Keresett slugok: " . implode(' / ', $slugs));
$parent = null;
foreach ($slugs as $slug) {
$query = Category::where('slug', $slug);
if ($parent) {
$query->where('parent_id', $parent->id);
} else {
$query->whereNull('parent_id');
}
$parent = $query->first();
if (!$parent) {
Log::error("❌ HIBA: Nem található kategória ezzel a sluggal: " . $slug);
return null;
}
}
return $parent;
}
private function buildCategorySlugPath($category)
{
if (!$category) return '';
$slugs = [];
while ($category) {
array_unshift($slugs, $category->slug);
$category = Category::find($category->parent_id);
}
return implode('/', $slugs);
}
public function toggleCategory($categoryId)
{
// Ha már nyitva van, csukjuk be, ha nincs nyitva, akkor csak ezt nyissuk ki
if (isset($this->openCategories[$categoryId])) {
unset($this->openCategories[$categoryId]);
} else {
// Csak az adott kategóriát és annak szülőit hagyjuk nyitva
$this->openCategories = [];
$category = Category::find($categoryId);
while ($category) {
$this->openCategories[$category->id] = true;
$category = Category::find($category->parent_id);
}
}
}
public function refreshCategory()
{
if ($this->selectedCategoryId) {
$this->selectCategory($this->selectedCategoryId);
}
}
public function render()
{
return view('livewire.product-page', [
'categorySlugPaths' => $this->categorySlugPaths,
]);
}
}
resources\views\livewire\product-page.blade.php
<div class="container">
<div class="grid grid-cols-4 gap-4">
<!-- Kategória lista -->
<div class="">
<h2 class="text-lg font-bold mb-3">Kategóriák</h2>
@include('livewire.partials.category-list', ['categories' => $categories, 'depth' => 0])
</div>
<!-- Termék lista -->
<div class="col-span-3">
<h2 class="text-lg font-bold mb-3">Termékek</h2>
@if(!$selectedCategoryId)
<p>Válassz egy kategóriát a termékek megtekintéséhez.</p>
@elseif(empty($products))
<p>Nincsenek termékek ebben a kategóriában.</p>
@else
<ul class="grid grid-cols-1 md:grid-cols-3 gap-4">
@foreach($products as $product)
<li class="border p-4 rounded shadow hover:shadow-lg transition">
<a href="{{ route('products.detail', ['categorySlugPath' => $categorySlugPaths[$product->id] ?? '', 'productSlug' => $product->slug]) }}"
class="block text-lg font-semibold text-blue-600 hover:underline">
{{ $product->name }}
</a>
</li>
@endforeach
</ul>
@endif
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
console.log("✅ DOM betöltve!");
window.addEventListener("popstate", function () {
console.log("🔄 Visszalépés érzékelve! Livewire frissítés.");
Livewire.dispatch("refreshCategory");
});
Livewire.on('replaceState', function (data) {
console.log("🔄 Livewire esemény fogadva! Adatok: ", data);
if (data && typeof data.url === 'string' && data.url.trim() !== "") {
try {
console.log("🔹 URL cseréje: ", data.url);
window.history.replaceState({}, '', data.url);
console.log("✅ URL sikeresen frissítve:", window.location.href);
} catch (error) {
console.error("❌ Hiba az URL frissítés közben:", error);
}
} else {
console.warn("⚠️ Nincs érvényes URL a replaceState eseményben!", data);
}
});
});
</script>
</div>
app\Livewire\ProductDetail.php
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Category;
use App\Models\Product;
class ProductDetail extends Component
{
public $product;
public $category;
public $categorySlugPath;
public function mount($categorySlugPath, $productSlug)
{
$slugs = explode('/', $categorySlugPath);
$this->category = $this->findCategoryBySlugs($slugs);
if (!$this->category) {
abort(404);
}
$this->product = Product::where('slug', $productSlug)
->where('category_id', $this->category->id)
->firstOrFail();
// $this->categorySlugPath = $categorySlugPath;
$this->categorySlugPath = $this->buildCategorySlugPath($this->category);
}
private function findCategoryBySlugs($slugs)
{
$parent = null;
foreach ($slugs as $slug) {
$query = Category::where('slug', $slug);
if ($parent) {
$query->where('parent_id', $parent->id);
} else {
$query->whereNull('parent_id');
}
$parent = $query->first();
if (!$parent) {
return null;
}
}
return $parent;
}
private function buildCategorySlugPath($category)
{
if (!$category) return '';
$slugs = [];
while ($category) {
array_unshift($slugs, $category->slug);
$category = Category::find($category->parent_id);
}
return implode('/', $slugs);
}
public function render()
{
return view('livewire.product-detail', [
'categorySlugPath' => $this->categorySlugPath,
]);
}
}
resources\views\livewire\product-detail.blade.php
<div class="container mx-auto py-10">
<div class="max-w-3xl mx-auto bg-white p-6 shadow rounded">
<h1 class="text-3xl font-bold mb-4">{{ $product->name }}</h1>
<p class="text-gray-600 mb-4">{{ $product->description }}</p>
<a href="{{ route('products.category', ['categorySlugPath' => $categorySlugPath]) }}"
class="text-blue-600 hover:underline mt-4 inline-block">
← Back to product list
</a>
</div>
</div>
routes\web.php
Route::get('/products', ProductPage::class)->name('products');
Route::get('/products/{categorySlugPath?}/{productSlug}', ProductDetail::class)
->where('categorySlugPath', '[a-zA-Z0-9-_/]+')
->where('productSlug', '[a-zA-Z0-9-_]+')
->name('products.detail');
Route::get('/products/{categorySlugPath?}', ProductPage::class)
->where('categorySlugPath', '[a-zA-Z0-9-_/]+')
->name('products.category');
resources\views\livewire\partials\category-list.blade.php
<ul class="ml-4">
@foreach($categories as $category)
<li class="cursor-pointer p-2 rounded flex justify-between items-center
{{ $selectedCategoryId === $category->id ? 'bg-blue-500 text-white' : 'hover:bg-gray-200' }}"
@if($category->children->count() > 0)
wire:click="toggleCategory({{ $category->id }})"
@else
wire:click="selectCategory({{ $category->id }})"
@endif>
<span>
{{ str_repeat('-', $depth) }} {{ $category->name }}
@if($category->children->count() === 0)
[{{ $category->products->count() }}]
@endif
</span>
@if($category->children->count() > 0)
<span class="ml-2 transition-transform transform
@if(isset($openCategories[$category->id])) rotate-180 @endif"
id="icon-{{ $category->id }}">▼</span>
@endif
</li>
@if($category->children->count())
<ul id="subcategory-{{ $category->id }}"
class="ml-4 transition-all duration-300 ease-in-out
@if(isset($openCategories[$category->id])) max-h-screen opacity-100 @else max-h-0 opacity-0 overflow-hidden @endif">
@include('livewire.partials.category-list', ['categories' => $category->children, 'depth' => $depth + 1])
</ul>
@endif
@endforeach
</ul>