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:
1 parent
6b2a67c4cd
commit
45b5f586ba
5 files changed
+166
-11
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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()) · {{ $searchShorts->count() }} short{{ $searchShorts->count() !== 1 ? 's' : '' }} @endif
|
||||
@if($searchShorts->count()) · {{ $searchShorts->count() }} short{{ $searchShorts->count() !== 1 ? 's' : '' }} @endif
|
||||
@if($searchChannels->count()) · {{ $searchChannels->count() }} channel{{ $searchChannels->count() !== 1 ? 's' : '' }} @endif
|
||||
@if($searchPlaylists->count()) · {{ $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">
|
||||
|
||||
Reference in new issue
Block a user