feat(channel): /@handle/{tab} aliases + cursor pagination on Videos tab

Two complementary additions that share the same bridge-not-rewrite
philosophy — reuse the existing single-page channel view instead of
splitting its 2000 lines into five route-driven partials.

/@handle/{tab}: videos / shorts / playlists / posts / about / wall
redirect to the equivalent /channel/{slug}#tab URL (302). The channel
page's existing hash-tab JS already handles landing on the right tab;
these aliases just make the URLs shareable in the YouTube style.

Channel Videos tab Load More: new JSON endpoint
GET /channel/{username}/videos/more returns the next cursor page of
server-rendered video cards. A sentinel + IntersectionObserver below
the grid fetches the next page when scrolled into view, with a Load
More button as fallback. Behavior mirrors the Phase 2 home feed —
users with thousands of uploads no longer get a 300-item grid on
first paint.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-10-21 12:33:05 +03:00
1 parent f7c3b236ab
commit 677e8711dd
3 files changed
+99 -1

No files matched your search

+33 -1
View File
@@ -18,7 +18,7 @@ class UserController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['channel']);
$this->middleware('auth')->except(['channel', 'channelVideosMore']);
}
// Typeahead search for members (used by the "Share to member" picker)
@@ -372,6 +372,38 @@ class UserController extends Controller
return redirect()->route('history')->with('toast_success', 'Watch history cleared.');
}
/**
* JSON endpoint powering the "Load more" button on the channel Videos
* tab. Takes a username + optional cursor and returns a page of
* server-rendered video cards + next cursor. Reuses the cursorPaginate
* pattern Phase 2 established for the home feed.
*/
public function channelVideosMore(Request $request, string $username)
{
$owner = User::where('username', $username)->firstOrFail();
$viewer = Auth::user();
$isOwner = $viewer && $viewer->id === $owner->id;
$query = $isOwner
? Video::where('user_id', $owner->id)
: Video::public()->where('user_id', $owner->id);
$paginator = $query->where('is_shorts', false)
->latest('id')
->cursorPaginate(24);
$html = '';
foreach ($paginator->items() as $video) {
$html .= view('components.video-card', ['video' => $video, 'hideChannelInfo' => true])->render();
}
return response()->json([
'items' => $html,
'next_cursor' => optional($paginator->nextCursor())->encode(),
'has_more' => $paginator->hasMorePages(),
]);
}
/**
* Subscriptions feed — the public videos uploaded by every channel the
* viewer is subscribed to, newest first. Replaces the dead href="#" that