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') ->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(function (Artwork $art) { $present = ThumbnailPresenter::present($art, 'md'); return [ 'id' => $art->id, 'name' => $art->title, 'thumb' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], 'width' => $art->width, 'height' => $art->height, 'username' => $art->user->username ?? null, 'uname' => $art->user->username ?? $art->user->name ?? 'Skinbase', 'published_at' => $art->published_at, ]; })->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 { if (! Schema::hasTable('user_favorites')) { 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; $cursor = $request->input('cursor'); $favIds = DB::table('user_favorites as uf') ->join('artworks as a', 'a.id', '=', 'uf.artwork_id') ->where('uf.user_id', $user->id) ->whereNull('a.deleted_at') ->where('a.is_public', true) ->where('a.is_approved', true) ->orderByDesc('uf.created_at') ->offset($cursor ? (int) base64_decode($cursor) : 0) ->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') ->whereIn('id', $favIds) ->get() ->keyBy('id'); $data = $favIds->filter(fn ($id) => $indexed->has($id))->map(function ($id) use ($indexed) { $art = $indexed[$id]; $present = ThumbnailPresenter::present($art, 'md'); return [ 'id' => $art->id, 'name' => $art->title, 'thumb' => $present['url'], 'thumb_srcset' => $present['srcset'] ?? $present['url'], 'width' => $art->width, 'height' => $art->height, 'username' => $art->user->username ?? null, 'uname' => $art->user->username ?? $art->user->name ?? 'Skinbase', ]; })->values(); return response()->json([ 'data' => $data, 'next_cursor' => null, // Simple offset pagination for now '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(); } }