resolveUser($username); if (! $user) { return response()->json(['error' => 'User not found'], 404); } $isOwner = Auth::check() && Auth::id() === $user->id; $sort = $request->input('sort', 'latest'); $query = Artwork::with([ 'user:id,name,username,level,rank', 'user.profile:user_id,avatar_hash', 'group:id,name,slug,avatar_path', 'stats:artwork_id,views,downloads,favorites', 'categories' => function ($query) { $query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order') ->with(['contentType:id,slug,name']); }, ]) ->where('user_id', $user->id) ->whereNull('deleted_at'); if (! $isOwner) { $query->where('is_public', true)->where('is_approved', true)->whereNotNull('published_at'); } $query = match ($sort) { 'trending' => $query->orderByDesc('ranking_score'), 'rising' => $query->orderByDesc('heat_score'), 'views' => $query->orderByDesc('view_count'), 'favs' => $query->orderByDesc('favourite_count'), default => $query->orderByDesc('published_at'), }; $perPage = 24; $paginator = $query->cursorPaginate($perPage); $data = collect($paginator->items()) ->map(fn (Artwork $art) => $this->mapArtworkCardPayload($art)) ->values(); return response()->json([ 'data' => $data, 'next_cursor' => $paginator->nextCursor()?->encode(), 'has_more' => $paginator->hasMorePages(), ]); } /** * GET /api/profile/{username}/favourites * Returns cursor-paginated favourites for the profile. */ public function favourites(Request $request, string $username): JsonResponse { $favouriteTable = $this->resolveFavouriteTable(); if ($favouriteTable === null) { return response()->json(['data' => [], 'next_cursor' => null, 'has_more' => false]); } $user = $this->resolveUser($username); if (! $user) { return response()->json(['error' => 'User not found'], 404); } $perPage = 24; $offset = max(0, (int) base64_decode((string) $request->input('cursor', ''), true)); $favIds = DB::table($favouriteTable . ' as af') ->join('artworks as a', 'a.id', '=', 'af.artwork_id') ->where('af.user_id', $user->id) ->whereNull('a.deleted_at') ->where('a.is_public', true) ->where('a.is_approved', true) ->whereNotNull('a.published_at') ->orderByDesc('af.created_at') ->orderByDesc('af.artwork_id') ->offset($offset) ->limit($perPage + 1) ->pluck('a.id'); $hasMore = $favIds->count() > $perPage; $favIds = $favIds->take($perPage); if ($favIds->isEmpty()) { return response()->json(['data' => [], 'next_cursor' => null, 'has_more' => false]); } $indexed = Artwork::with([ 'user:id,name,username,level,rank', 'user.profile:user_id,avatar_hash', 'group:id,name,slug,avatar_path', 'stats:artwork_id,views,downloads,favorites', 'categories' => function ($query) { $query->select('categories.id', 'categories.content_type_id', 'categories.parent_id', 'categories.name', 'categories.slug', 'categories.sort_order') ->with(['contentType:id,slug,name']); }, ]) ->whereIn('id', $favIds) ->get() ->keyBy('id'); $data = $favIds ->filter(fn ($id) => $indexed->has($id)) ->map(fn ($id) => $this->mapArtworkCardPayload($indexed[$id])) ->values(); return response()->json([ 'data' => $data, 'next_cursor' => $hasMore ? base64_encode((string) ($offset + $perPage)) : null, 'has_more' => $hasMore, ]); } /** * GET /api/profile/{username}/stats * Returns profile statistics. */ public function stats(Request $request, string $username): JsonResponse { $user = $this->resolveUser($username); if (! $user) { return response()->json(['error' => 'User not found'], 404); } $stats = null; if (Schema::hasTable('user_statistics')) { $stats = DB::table('user_statistics')->where('user_id', $user->id)->first(); } $followerCount = 0; if (Schema::hasTable('user_followers')) { $followerCount = DB::table('user_followers')->where('user_id', $user->id)->count(); } return response()->json([ 'stats' => $stats, 'follower_count' => $followerCount, ]); } private function resolveUser(string $username): ?User { $normalized = UsernamePolicy::normalize($username); return User::query()->whereRaw('LOWER(username) = ?', [$normalized])->first(); } private function resolveFavouriteTable(): ?string { foreach (['artwork_favourites', 'user_favorites', 'artworks_favourites', 'favourites'] as $table) { if (Schema::hasTable($table)) { return $table; } } return null; } /** * @return array */ private function mapArtworkCardPayload(Artwork $art): array { $present = ThumbnailPresenter::present($art, 'md'); $category = $art->categories->first(); $contentType = $category?->contentType; $stats = $art->stats; $group = $art->group; $isGroupPublisher = $group !== null; $displayName = $isGroupPublisher ? ($group->name ?? 'Skinbase') : ($art->user?->name ?? 'Skinbase'); $username = $isGroupPublisher ? null : ($art->user?->username ?? null); $avatarUrl = $isGroupPublisher ? $group->avatarUrl() : ($art->user?->profile?->avatar_url ?? null); $profileUrl = $isGroupPublisher ? $group->publicUrl() : ($username ? '/@' . $username : null); $publisherType = $isGroupPublisher ? 'group' : 'user'; return [ 'id' => $art->id, 'name' => $art->title, 'thumb' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], 'width' => $art->width, 'height' => $art->height, 'username' => $username, 'uname' => $displayName, 'avatar_url' => $avatarUrl, 'profile_url' => $profileUrl, 'published_as_type' => $publisherType, 'publisher' => [ 'type' => $publisherType, 'id' => $isGroupPublisher ? (int) $group->id : (int) ($art->user?->id ?? 0), 'name' => $displayName, 'username' => $username ?? '', 'avatar_url' => $avatarUrl, 'profile_url' => $profileUrl, ], 'user_id' => $art->user_id, 'author_level' => $isGroupPublisher ? 0 : (int) ($art->user?->level ?? 1), 'author_rank' => $isGroupPublisher ? '' : (string) ($art->user?->rank ?? 'Newbie'), 'content_type' => $contentType?->name, 'content_type_slug' => $contentType?->slug, 'category' => $category?->name, 'category_slug' => $category?->slug, 'views' => (int) ($stats?->views ?? $art->view_count ?? 0), 'downloads' => (int) ($stats?->downloads ?? 0), 'likes' => (int) ($stats?->favorites ?? $art->favourite_count ?? 0), 'published_at' => $this->formatIsoDate($art->published_at), ]; } private function formatIsoDate(mixed $value): ?string { if ($value instanceof CarbonInterface) { return $value->toISOString(); } if ($value instanceof \DateTimeInterface) { return $value->format(DATE_ATOM); } return is_string($value) ? $value : null; } }