diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php new file mode 100644 index 0000000..3fdbe80 --- /dev/null +++ b/app/Http/Controllers/SearchController.php @@ -0,0 +1,96 @@ +get('q', '')); + + if ($query === '') { + // No query → send the user back to the browse feed instead of an + // empty "no results" page. + return redirect()->route('videos.index'); + } + + $like = '%' . $query . '%'; + $qLower = mb_strtolower($query); + + // ── Videos ──────────────────────────────────────────────── + // Match on text columns AND on any audio-track language that aligns + // with the query (e.g. searching "arabic" surfaces videos that carry + // an Arabic track even if the primary language is English). + $trackVideoIds = VideoAudioTrack::whereRaw('LOWER(language) = ?', [$qLower]) + ->pluck('video_id'); + + $videoHits = Video::public() + ->where(function ($q) use ($like, $qLower, $trackVideoIds) { + $q->where('title', 'like', $like) + ->orWhere('description','like', $like) + ->orWhere('tags', 'like', $like) + ->orWhereRaw('LOWER(language) = ?', [$qLower]) + ->orWhereIn('id', $trackVideoIds); + }) + ->latest() + ->limit(60) + ->get(); + + $videos = $videoHits->where('is_shorts', false)->values(); + $shorts = $videoHits->where('is_shorts', true)->values(); + + // ── Channels ────────────────────────────────────────────── + $channels = User::where(function ($q) use ($like) { + $q->where('name', 'like', $like) + ->orWhere('username','like', $like) + ->orWhere('bio', 'like', $like); + }) + ->orderByDesc('id') + ->limit(12) + ->get(); + + // ── Playlists ───────────────────────────────────────────── + $playlists = Playlist::where('visibility', 'public') + ->where(function ($q) use ($like) { + $q->where('name', 'like', $like) + ->orWhere('description','like', $like); + }) + ->withCount('videos') + ->with(['user', 'videos' => fn ($q) => $q->orderBy('playlist_videos.position')->limit(1)]) + ->latest() + ->limit(12) + ->get(); + + return view('videos.index', [ + 'videos' => $videos, + 'shorts' => $shorts, + 'playlists' => $playlists, + 'channels' => $channels, + 'query' => $query, + 'langChips' => [], + ]); + } +} diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 30a1186..4491d28 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -1237,10 +1237,11 @@
-
- -
diff --git a/resources/views/layouts/partials/header.blade.php b/resources/views/layouts/partials/header.blade.php index 2b08655..4c89af4 100644 --- a/resources/views/layouts/partials/header.blade.php +++ b/resources/views/layouts/partials/header.blade.php @@ -15,9 +15,10 @@
diff --git a/resources/views/videos/index.blade.php b/resources/views/videos/index.blade.php index fcb4e7e..219f41e 100644 --- a/resources/views/videos/index.blade.php +++ b/resources/views/videos/index.blade.php @@ -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

Search results for "{{ $query }}"

@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

+@if($searchChannels->count()) +
Channels
+
+ @foreach($searchChannels as $channel) + + @if($channel->avatar_url) + {{ $channel->name }} + @else +
+ @endif +
+
{{ $channel->name }}
+ @if($channel->username) +
@{{ $channel->username }}
+ @endif +
+
+ @endforeach +
+@endif + @if($videos->count())
Videos
diff --git a/routes/web.php b/routes/web.php index f2d3039..09f363d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,7 +25,13 @@ Route::get('/', [VideoController::class, 'index'])->name('home'); // Video routes - public Route::get('/videos', [VideoController::class, 'index'])->name('videos.index'); Route::get('/videos/feed/more', [VideoController::class, 'feedMore'])->name('videos.feed.more'); -Route::get('/videos/search', [VideoController::class, 'search'])->name('videos.search'); + +// Site-wide search. /videos/search is the historical URL; keep it as a +// signed-forward redirect so bookmarks and external links don't break. +Route::get('/search', [\App\Http\Controllers\SearchController::class, 'index'])->name('search'); +Route::get('/videos/search', function (\Illuminate\Http\Request $request) { + return redirect()->route('search', $request->only('q'), 301); +})->name('videos.search'); Route::get('/trending', [VideoController::class, 'trending'])->name('videos.trending'); Route::get('/shorts', [VideoController::class, 'shorts'])->name('videos.shorts'); Route::get('/videos/create', [VideoController::class, 'create'])->name('videos.create');