feat(home): Shorts shelf above the main feed

The home grid used to interleave shorts with regular videos ordered by
upload date, so a cluster of 60-second portrait clips could fill the
first row and push the non-shorts off the fold. Split them:

- The main feed grid now excludes shorts (notShorts() filter).
- A horizontally-scrolling Shorts shelf renders above the grid with
  up to 12 latest shorts, each a real <a href> to /shorts/{id} so
  tapping it deep-links the vertical feed to that short.
- "See all" link in the shelf header jumps to the full /shorts feed.
- Shelf is skipped entirely when there are no shorts to show.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-10-21 10:31:27 +03:00
1 parent 9e613c05d2
commit c3955c4a7c
2 files changed
+61 -2

No files matched your search

+7 -2
View File
@@ -176,7 +176,12 @@ class VideoController extends Controller
} }
// ── All (home) — mixed chronological feed ───────────────── // ── All (home) — mixed chronological feed ─────────────────
$videos = Video::public()->latest()->limit(60)->get(); // Videos shown in the grid are the newest non-shorts. Shorts render
// in their own horizontally-scrolling shelf injected after the first
// row of cards (see videos/index.blade.php), so the home grid isn't
// diluted by 60-second portrait clips.
$videos = Video::public()->notShorts()->latest()->limit(60)->get();
$homeShorts = Video::public()->where('is_shorts', true)->latest()->limit(12)->get();
$playlistQuery = Playlist::withCount('videos') $playlistQuery = Playlist::withCount('videos')
->with(['user', 'videos' => fn($q) => $q->orderBy('playlist_videos.position')->limit(1)]) ->with(['user', 'videos' => fn($q) => $q->orderBy('playlist_videos.position')->limit(1)])
@@ -193,7 +198,7 @@ class VideoController extends Controller
->sortByDesc('date') ->sortByDesc('date')
->values(); ->values();
return view('videos.index', compact('feedItems', 'filter', 'langChips') + [ return view('videos.index', compact('feedItems', 'filter', 'langChips', 'homeShorts') + [
'videos' => collect(), 'shorts' => collect(), 'matches' => collect(), 'playlists' => collect(), 'videos' => collect(), 'shorts' => collect(), 'matches' => collect(), 'playlists' => collect(),
]); ]);
} }
+54
View File
@@ -237,6 +237,60 @@
══════════════════════════════════════════════ --}} ══════════════════════════════════════════════ --}}
@elseif(!isset($query) && (!isset($filter) || $filter === 'all')) @elseif(!isset($query) && (!isset($filter) || $filter === 'all'))
@if(!empty($homeShorts) && $homeShorts->count())
{{-- Horizontally-scrolling Shorts shelf. Sits above the main feed grid
so the user sees fresh short-form content without the feed being
drowned in 60-second portrait clips. --}}
<section class="home-shorts-shelf" aria-labelledby="home-shorts-heading">
<div class="home-shorts-shelf-head">
<h2 id="home-shorts-heading"><i class="bi bi-lightning-charge-fill" aria-hidden="true"></i> Shorts</h2>
<a href="{{ route('videos.shorts') }}" class="home-shorts-see-all">See all</a>
</div>
<div class="home-shorts-shelf-track">
@foreach($homeShorts as $s)
<a class="home-short-card" href="{{ route('videos.shorts.focused', $s) }}"
aria-label="Open short: {{ $s->title }}">
<img src="{{ $s->thumbnail ? route('media.thumbnail', $s->thumbnail) : 'https://picsum.photos/seed/'.$s->id.'/180/320' }}"
alt="" width="150" height="266" loading="lazy" decoding="async">
<div class="home-short-card-overlay">
<div class="home-short-card-title">{{ Str::limit($s->title, 40) }}</div>
<div class="home-short-card-views">{{ number_format($s->view_count) }} views</div>
</div>
</a>
@endforeach
</div>
</section>
<style>
.home-shorts-shelf { margin: 8px 0 24px; }
.home-shorts-shelf-head { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:12px; }
.home-shorts-shelf-head h2 { font-size:18px; font-weight:600; margin:0; display:flex; align-items:center; gap:8px; }
.home-shorts-shelf-head h2 i { color:#ef4444; }
.home-shorts-see-all { color: var(--text-secondary); font-size:13px; text-decoration:none; }
.home-shorts-see-all:hover { color: var(--text-primary); text-decoration: underline; }
.home-shorts-shelf-track {
display:flex; gap:12px; overflow-x:auto; scroll-snap-type: x proximity;
scrollbar-width: none; -ms-overflow-style: none;
padding-bottom: 4px;
}
.home-shorts-shelf-track::-webkit-scrollbar { display:none; }
.home-short-card {
flex: 0 0 150px; aspect-ratio: 9/16; border-radius: 10px;
overflow: hidden; position: relative; scroll-snap-align: start;
text-decoration: none; color: inherit; background: #1a1a1a;
transition: transform .15s ease;
}
.home-short-card:hover { transform: translateY(-2px); }
.home-short-card img { width:100%; height:100%; object-fit: cover; display:block; }
.home-short-card-overlay {
position:absolute; left:0; right:0; bottom:0; padding: 20px 8px 8px;
background: linear-gradient(transparent, rgba(0,0,0,.75));
color: #fff;
}
.home-short-card-title { font-size:12px; font-weight:600; line-height:1.3; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; }
.home-short-card-views { font-size:11px; opacity:.8; margin-top:2px; }
</style>
@endif
@if(isset($feedItems) && $feedItems->count()) @if(isset($feedItems) && $feedItems->count())
<div class="yt-video-grid"> <div class="yt-video-grid">
@foreach($feedItems as $entry) @foreach($feedItems as $entry)