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> $categories * @return Collection> */ 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 $categories * @return array> */ 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(); } }