feat(search): /search?q= across videos, channels and playlists

Add a dedicated SearchController that matches the query against video
title / description / tags / primary language AND any secondary audio
track language (so "arabic" surfaces videos that carry an Arabic track
even if the primary language is English), plus channel name / username
/ bio, plus playlist name / description. Public visibility only.

Results reuse the videos/index view so every card gets the same
component the browse feeds use. A new Channels section renders above
the Videos grid when channel hits exist.

/videos/search is kept as a 301 redirect to /search so bookmarks and
external links stay valid; both the desktop header and mobile overlay
search forms now point at the canonical route, both carry a visually
hidden <label>, both get role="search" and both preserve the current
query in their input.

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:09:40 +03:00
1 parent 6b2a67c4cd
commit 45b5f586ba
5 files changed
+166 -11

No files matched your search

+5 -4
View File
@@ -1237,10 +1237,11 @@
<!-- Mobile Search Overlay -->
<div class="mobile-search-overlay" id="mobileSearchOverlay">
<form action="{{ route('videos.search') }}" method="GET" class="mobile-search-form">
<input type="text" name="q" class="mobile-search-input" placeholder="Search" value="{{ request('q') }}">
<button type="submit" class="mobile-search-submit">
<i class="bi bi-search"></i>
<form action="{{ route('search') }}" method="GET" class="mobile-search-form" role="search">
<label for="mobile-search-input" class="visually-hidden">Search</label>
<input type="search" id="mobile-search-input" name="q" class="mobile-search-input" placeholder="Search" value="{{ request('q') }}" aria-label="Search videos, channels and playlists">
<button type="submit" class="mobile-search-submit" aria-label="Submit search">
<i class="bi bi-search" aria-hidden="true"></i>
</button>
</form>
</div>
@@ -15,9 +15,10 @@
<div class="yt-header-center d-none d-md-flex">
<div class="yt-search">
<form action="{{ route('videos.search') }}" method="GET" class="yt-search-form">
<input type="text" name="q" class="yt-search-input" placeholder="Search" value="{{ request('q') }}">
<button type="submit" class="yt-search-btn"><i class="bi bi-search"></i></button>
<form action="{{ route('search') }}" method="GET" class="yt-search-form" role="search">
<label for="header-search-input" class="visually-hidden">Search</label>
<input type="search" id="header-search-input" name="q" class="yt-search-input" placeholder="Search" value="{{ request('q') }}" aria-label="Search videos, channels and playlists">
<button type="submit" class="yt-search-btn" aria-label="Submit search"><i class="bi bi-search" aria-hidden="true"></i></button>
</form>
</div>
</div>
+54 -3
View File
@@ -42,6 +42,34 @@
}
.results-section-title::after { content: ''; flex: 1; height: 1px; background: var(--border-color); }
/* Channel results — card list sized to sit comfortably above the video
grid. Avatar is a fixed 56px circle; name + handle sit to the right. */
.search-channels-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 12px;
margin-bottom: 8px;
}
.search-channel-card {
display: flex; align-items: center; gap: 12px;
padding: 10px 12px;
background: var(--bg-secondary);
border-radius: 12px;
text-decoration: none;
color: var(--text-primary);
transition: background .15s ease;
}
.search-channel-card:hover { background: rgba(255,255,255,.08); }
.search-channel-card img,
.search-channel-ph {
width: 56px; height: 56px; border-radius: 50%; object-fit: cover; flex: 0 0 auto;
background: #333; display: flex; align-items: center; justify-content: center;
font-size: 22px; color: var(--text-secondary);
}
.search-channel-text { min-width: 0; }
.search-channel-name { font-weight: 600; font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.search-channel-handle { font-size: 13px; color: var(--text-secondary); }
.yt-empty { text-align: center; padding: 80px 20px; }
/* Language chips */
@@ -112,21 +140,44 @@
@isset($query)
@php
$searchShorts = $shorts ?? collect();
$searchShorts = $shorts ?? collect();
$searchPlaylists = $playlists ?? collect();
$totalResults = $videos->count() + $searchShorts->count() + $searchPlaylists->count();
$searchChannels = $channels ?? collect();
$totalResults = $videos->count() + $searchShorts->count() + $searchPlaylists->count() + $searchChannels->count();
@endphp
<div class="search-info">
<h2>Search results for "{{ $query }}"</h2>
<p>
@if($videos->count()) {{ $videos->count() }} video{{ $videos->count() !== 1 ? 's' : '' }} @endif
@if($searchShorts->count()) &nbsp;·&nbsp; {{ $searchShorts->count() }} short{{ $searchShorts->count() !== 1 ? 's' : '' }} @endif
@if($searchShorts->count()) &nbsp;·&nbsp; {{ $searchShorts->count() }} short{{ $searchShorts->count() !== 1 ? 's' : '' }} @endif
@if($searchChannels->count()) &nbsp;·&nbsp; {{ $searchChannels->count() }} channel{{ $searchChannels->count() !== 1 ? 's' : '' }} @endif
@if($searchPlaylists->count()) &nbsp;·&nbsp; {{ $searchPlaylists->count() }} playlist{{ $searchPlaylists->count() !== 1 ? 's' : '' }} @endif
@if($totalResults === 0) No results found @endif
</p>
</div>
@if($searchChannels->count())
<div class="results-section-title"><i class="bi bi-person-circle"></i> Channels</div>
<div class="search-channels-grid">
@foreach($searchChannels as $channel)
<a href="{{ route('channel', $channel->channel) }}" class="search-channel-card">
@if($channel->avatar_url)
<img src="{{ $channel->avatar_url }}" alt="{{ $channel->name }}" width="56" height="56" loading="lazy">
@else
<div class="search-channel-ph"><i class="bi bi-person"></i></div>
@endif
<div class="search-channel-text">
<div class="search-channel-name">{{ $channel->name }}</div>
@if($channel->username)
<div class="search-channel-handle">@{{ $channel->username }}</div>
@endif
</div>
</a>
@endforeach
</div>
@endif
@if($videos->count())
<div class="results-section-title"><i class="bi bi-play-circle"></i> Videos</div>
<div class="yt-video-grid">