202 lines
8.5 KiB
PHP
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();
|
|
}
|
|
}
|