Add trending videos page with YouTube-style algorithm
- Trending algorithm based on: - Recent views (last 48h): 70% weight - View velocity (views/hour): 15% weight - Recency bonus: 10% weight - Like count: 5% weight - Excludes videos older than 10 days - Filter options: Today/This Week/This Month - Added route, controller method, and view - Updated nav to link to trending page
This commit is contained in:
1 parent
59870862db
commit
9ad842dcd5
5 files changed
+327
-2
No files matched your search
@@ -415,7 +415,7 @@
|
||||
|
||||
<!-- Mobile Search Overlay -->
|
||||
<div class="mobile-search-overlay" id="mobileSearchOverlay">
|
||||
<form action="{{ route('videos.search') }}" method="GET" class="mobile-search-form">
|
||||
<form action="{{ route('videos.trending') }}" 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>
|
||||
@@ -494,7 +494,7 @@
|
||||
<i class="bi bi-house-door-fill"></i>
|
||||
<span>Home</span>
|
||||
</a>
|
||||
<a href="{{ route('videos.search') }}" class="yt-bottom-nav-item {{ request()->routeIs('videos.search') ? 'active' : '' }}">
|
||||
<a href="{{ route('videos.trending') }}" class="yt-bottom-nav-item {{ request()->routeIs('videos.trending') ? 'active' : '' }}">
|
||||
<i class="bi bi-fire"></i>
|
||||
<span>Trending</span>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', 'Trending Videos | ' . config('app.name'))
|
||||
|
||||
@section('extra_styles')
|
||||
<style>
|
||||
.trending-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.trending-header h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.trending-icon {
|
||||
color: #f00;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.trending-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.trending-filters a {
|
||||
padding: 6px 14px;
|
||||
border-radius: 18px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.trending-filters a:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.trending-filters a.active {
|
||||
background: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.trending-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
color: white;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.trending-badge i { color: #f00; }
|
||||
|
||||
.yt-video-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.yt-video-card { cursor: pointer; }
|
||||
|
||||
.yt-video-thumb {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.yt-video-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.yt-video-card:hover .yt-video-thumb img { transform: scale(1.05); }
|
||||
|
||||
.yt-video-duration {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
color: white;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.yt-video-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.yt-video-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.yt-video-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.yt-video-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.yt-video-channel {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.yt-video-channel a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.yt-video-stats { font-size: 13px; color: var(--text-secondary); }
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.yt-video-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.trending-header { flex-wrap: wrap; }
|
||||
.trending-filters { margin-left: 0; width: 100%; margin-top: 12px; }
|
||||
.yt-video-grid { grid-template-columns: 1fr; gap: 20px; }
|
||||
}
|
||||
|
||||
.empty-trending {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-trending i { font-size: 64px; margin-bottom: 16px; opacity: 0.5; }
|
||||
.empty-trending h3 { font-size: 20px; color: var(--text-primary); margin-bottom: 8px; }
|
||||
</style>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="trending-header">
|
||||
<h1><i class="bi bi-fire trending-icon"></i> Trending</h1>
|
||||
<div class="trending-filters">
|
||||
<a href="{{ route('videos.trending', ['hours' => 24]) }}" class="{{ == 24 ? 'active' : '' }}">Today</a>
|
||||
<a href="{{ route('videos.trending', ['hours' => 48]) }}" class="{{ == 48 ? 'active' : '' }}">This Week</a>
|
||||
<a href="{{ route('videos.trending', ['hours' => 168]) }}" class="{{ == 168 ? 'active' : '' }}">This Month</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(())
|
||||
<div class="empty-trending">
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<h3>No trending videos yet</h3>
|
||||
<p>Videos with high engagement will appear here</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="yt-video-grid">
|
||||
@foreach( as )
|
||||
<a href="{{ route('videos.show', ) }}" class="yt-video-card">
|
||||
<div class="yt-video-thumb">
|
||||
<img src="{{ }}" alt="{{ }}" loading="lazy">
|
||||
<span class="trending-badge"><i class="bi bi-fire"></i> Trending</span>
|
||||
@if()<span class="yt-video-duration">{{ gmdate('i:s', ) }}</span>@endif
|
||||
</div>
|
||||
<div class="yt-video-meta">
|
||||
@if()<img src="{{ ->avatar_url }}" alt="{{ ->name }}" class="yt-video-avatar">@endif
|
||||
<div class="yt-video-info">
|
||||
<h3 class="yt-video-title">{{ }}</h3>
|
||||
<div class="yt-video-channel"><a href="#">{{ ->name ?? 'Unknown' }}</a></div>
|
||||
<div class="yt-video-stats">
|
||||
{{ number_format() }} views
|
||||
@if( > 0) • {{ number_format() }} likes @endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
Reference in new issue
Block a user