categories v1 finished
This commit is contained in:
201
app/Http/Controllers/CategoryController.php
Normal file
201
app/Http/Controllers/CategoryController.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Services\ThumbnailService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$search = trim((string) $request->query('q', ''));
|
||||
$sort = (string) $request->query('sort', 'popular');
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$perPage = min(60, max(12, (int) $request->query('per_page', 24)));
|
||||
|
||||
$categories = collect(Cache::remember('categories.directory.v1', 3600, function (): array {
|
||||
$publishedArtworkScope = DB::table('artwork_category as artwork_category')
|
||||
->join('artworks as artworks', 'artworks.id', '=', 'artwork_category.artwork_id')
|
||||
->leftJoin('artwork_stats as artwork_stats', 'artwork_stats.artwork_id', '=', 'artworks.id')
|
||||
->whereColumn('artwork_category.category_id', 'categories.id')
|
||||
->where('artworks.is_public', true)
|
||||
->where('artworks.is_approved', true)
|
||||
->whereNull('artworks.deleted_at');
|
||||
|
||||
$categories = Category::query()
|
||||
->select([
|
||||
'categories.id',
|
||||
'categories.content_type_id',
|
||||
'categories.parent_id',
|
||||
'categories.name',
|
||||
'categories.slug',
|
||||
])
|
||||
->selectSub(
|
||||
(clone $publishedArtworkScope)->selectRaw('COUNT(DISTINCT artworks.id)'),
|
||||
'artwork_count'
|
||||
)
|
||||
->selectSub(
|
||||
(clone $publishedArtworkScope)
|
||||
->whereNotNull('artworks.hash')
|
||||
->whereNotNull('artworks.thumb_ext')
|
||||
->orderByDesc(DB::raw('COALESCE(artwork_stats.views, 0)'))
|
||||
->orderByDesc(DB::raw('COALESCE(artwork_stats.favorites, 0)'))
|
||||
->orderByDesc(DB::raw('COALESCE(artwork_stats.downloads, 0)'))
|
||||
->orderByDesc(DB::raw('COALESCE(artworks.published_at, artworks.created_at)'))
|
||||
->orderByDesc('artworks.id')
|
||||
->limit(1)
|
||||
->select('artworks.hash'),
|
||||
'cover_hash'
|
||||
)
|
||||
->selectSub(
|
||||
(clone $publishedArtworkScope)
|
||||
->whereNotNull('artworks.hash')
|
||||
->whereNotNull('artworks.thumb_ext')
|
||||
->orderByDesc(DB::raw('COALESCE(artwork_stats.views, 0)'))
|
||||
->orderByDesc(DB::raw('COALESCE(artwork_stats.favorites, 0)'))
|
||||
->orderByDesc(DB::raw('COALESCE(artwork_stats.downloads, 0)'))
|
||||
->orderByDesc(DB::raw('COALESCE(artworks.published_at, artworks.created_at)'))
|
||||
->orderByDesc('artworks.id')
|
||||
->limit(1)
|
||||
->select('artworks.thumb_ext'),
|
||||
'cover_ext'
|
||||
)
|
||||
->selectSub(
|
||||
(clone $publishedArtworkScope)
|
||||
->selectRaw('COALESCE(SUM(COALESCE(artwork_stats.views, 0) + (COALESCE(artwork_stats.favorites, 0) * 3) + (COALESCE(artwork_stats.downloads, 0) * 2)), 0)'),
|
||||
'popular_score'
|
||||
)
|
||||
->with(['contentType:id,name,slug'])
|
||||
->active()
|
||||
->orderBy('categories.name')
|
||||
->get();
|
||||
|
||||
return $this->transformCategories($categories);
|
||||
}));
|
||||
|
||||
$filtered = $this->filterAndSortCategories($categories, $search, $sort);
|
||||
$total = $filtered->count();
|
||||
$lastPage = max(1, (int) ceil($total / $perPage));
|
||||
$currentPage = min($page, $lastPage);
|
||||
$offset = ($currentPage - 1) * $perPage;
|
||||
$pageItems = $filtered->slice($offset, $perPage)->values();
|
||||
$popularCategories = $this->filterAndSortCategories($categories, '', 'popular')->take(4)->values();
|
||||
|
||||
return response()->json([
|
||||
'data' => $pageItems,
|
||||
'meta' => [
|
||||
'current_page' => $currentPage,
|
||||
'last_page' => $lastPage,
|
||||
'per_page' => $perPage,
|
||||
'total' => $total,
|
||||
],
|
||||
'summary' => [
|
||||
'total_categories' => $categories->count(),
|
||||
'total_artworks' => $categories->sum(fn (array $category): int => (int) ($category['artwork_count'] ?? 0)),
|
||||
],
|
||||
'popular_categories' => $search === '' ? $popularCategories : [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, array<string, mixed>> $categories
|
||||
* @return Collection<int, array<string, mixed>>
|
||||
*/
|
||||
private function filterAndSortCategories(Collection $categories, string $search, string $sort): Collection
|
||||
{
|
||||
$filtered = $categories;
|
||||
|
||||
if ($search !== '') {
|
||||
$needle = mb_strtolower($search);
|
||||
|
||||
$filtered = $filtered->filter(function (array $category) use ($needle): bool {
|
||||
return str_contains(mb_strtolower((string) ($category['name'] ?? '')), $needle);
|
||||
});
|
||||
}
|
||||
|
||||
return $filtered->sort(function (array $left, array $right) use ($sort): int {
|
||||
if ($sort === 'az') {
|
||||
return strcasecmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
|
||||
}
|
||||
|
||||
if ($sort === 'artworks') {
|
||||
$countCompare = ((int) ($right['artwork_count'] ?? 0)) <=> ((int) ($left['artwork_count'] ?? 0));
|
||||
|
||||
return $countCompare !== 0
|
||||
? $countCompare
|
||||
: strcasecmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
|
||||
}
|
||||
|
||||
$scoreCompare = ((int) ($right['popular_score'] ?? 0)) <=> ((int) ($left['popular_score'] ?? 0));
|
||||
if ($scoreCompare !== 0) {
|
||||
return $scoreCompare;
|
||||
}
|
||||
|
||||
$countCompare = ((int) ($right['artwork_count'] ?? 0)) <=> ((int) ($left['artwork_count'] ?? 0));
|
||||
if ($countCompare !== 0) {
|
||||
return $countCompare;
|
||||
}
|
||||
|
||||
return strcasecmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
|
||||
})->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, Category> $categories
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function transformCategories(Collection $categories): array
|
||||
{
|
||||
$categoryMap = $categories->keyBy('id');
|
||||
$pathCache = [];
|
||||
|
||||
$buildPath = function (Category $category) use (&$buildPath, &$pathCache, $categoryMap): string {
|
||||
if (isset($pathCache[$category->id])) {
|
||||
return $pathCache[$category->id];
|
||||
}
|
||||
|
||||
if ($category->parent_id && $categoryMap->has($category->parent_id)) {
|
||||
$pathCache[$category->id] = $buildPath($categoryMap->get($category->parent_id)) . '/' . $category->slug;
|
||||
|
||||
return $pathCache[$category->id];
|
||||
}
|
||||
|
||||
$pathCache[$category->id] = $category->slug;
|
||||
|
||||
return $pathCache[$category->id];
|
||||
};
|
||||
|
||||
return $categories
|
||||
->map(function (Category $category) use ($buildPath): array {
|
||||
$contentTypeSlug = strtolower((string) ($category->contentType?->slug ?? 'categories'));
|
||||
$path = $buildPath($category);
|
||||
$coverImage = null;
|
||||
|
||||
if (! empty($category->cover_hash) && ! empty($category->cover_ext)) {
|
||||
$coverImage = ThumbnailService::fromHash((string) $category->cover_hash, (string) $category->cover_ext, 'md');
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $category->id,
|
||||
'name' => (string) $category->name,
|
||||
'slug' => (string) $category->slug,
|
||||
'url' => '/' . $contentTypeSlug . '/' . $path,
|
||||
'content_type' => [
|
||||
'name' => (string) ($category->contentType?->name ?? 'Categories'),
|
||||
'slug' => $contentTypeSlug,
|
||||
],
|
||||
'cover_image' => $coverImage ?: 'https://files.skinbase.org/default/missing_md.webp',
|
||||
'artwork_count' => (int) ($category->artwork_count ?? 0),
|
||||
'popular_score' => (int) ($category->popular_score ?? 0),
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,25 @@ class CategoryController extends Controller
|
||||
|
||||
public function browseCategories()
|
||||
{
|
||||
$data = app(\App\Services\LegacyService::class)->browseCategories();
|
||||
return view('web.categories', $data);
|
||||
$pageTitle = 'All Categories – Wallpapers, Skins & Digital Art | Skinbase';
|
||||
$pageDescription = 'Browse all categories on Skinbase including wallpapers, skins, themes, and digital art collections.';
|
||||
|
||||
return view('web.categories', [
|
||||
'page_title' => $pageTitle,
|
||||
'page_meta_description' => $pageDescription,
|
||||
'page_canonical' => url('/categories'),
|
||||
'structured_data' => [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => 'Categories',
|
||||
'description' => $pageDescription,
|
||||
'url' => url('/categories'),
|
||||
'isPartOf' => [
|
||||
'@type' => 'WebSite',
|
||||
'name' => 'Skinbase',
|
||||
'url' => url('/'),
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user