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(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') ->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'); 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' => $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; } }