Files
SkinbaseNova/app/Http/Controllers/CategoryController.php
2026-03-17 20:13:33 +01:00

202 lines
8.5 KiB
PHP

<?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();
}
}