509 lines
No EOL
16 KiB
HTML
509 lines
No EOL
16 KiB
HTML
{% extends "layout.html" %}
|
|
|
|
{% block content %}
|
|
<script>
|
|
window.APP_CONFIG = {
|
|
page: '{{ page|default("home") }}',
|
|
channelId: '{{ channel_id|default("") }}',
|
|
query: '{{ query|default("") }}'
|
|
};
|
|
</script>
|
|
<!-- Filters & Categories -->
|
|
<div class="yt-filter-bar">
|
|
<div class="yt-categories" id="categoryList">
|
|
<!-- "All" removed, starting with Tech -->
|
|
<button class="yt-chip" onclick="switchCategory('tech')">Tech</button>
|
|
<button class="yt-chip" onclick="switchCategory('music')">Music</button>
|
|
<button class="yt-chip" onclick="switchCategory('movies')">Movies</button>
|
|
<button class="yt-chip" onclick="switchCategory('news')">News</button>
|
|
<button class="yt-chip" onclick="switchCategory('trending')">Trending</button>
|
|
<button class="yt-chip" onclick="switchCategory('podcasts')">Podcasts</button>
|
|
<button class="yt-chip" onclick="switchCategory('live')">Live</button>
|
|
<button class="yt-chip" onclick="switchCategory('gaming')">Gaming</button>
|
|
<button class="yt-chip" onclick="switchCategory('sports')">Sports</button>
|
|
</div>
|
|
|
|
<div class="yt-filter-actions">
|
|
<div class="yt-dropdown">
|
|
<button class="yt-icon-btn" id="filterToggleBtn" onclick="toggleFilterMenu()">
|
|
<i class="fas fa-sliders-h"></i>
|
|
</button>
|
|
<div class="yt-dropdown-menu" id="filterMenu">
|
|
<div class="yt-menu-section">
|
|
<h4>Sort By</h4>
|
|
<button onclick="changeSort('day')">Today</button>
|
|
<button onclick="changeSort('week')">This Week</button>
|
|
<button onclick="changeSort('month')">This Month</button>
|
|
<button onclick="changeSort('3months')">Last 3 Months</button>
|
|
<button onclick="changeSort('year')">This Year</button>
|
|
</div>
|
|
<div class="yt-menu-section">
|
|
<h4>Region</h4>
|
|
<button onclick="changeRegion('vietnam')">Vietnam</button>
|
|
<button onclick="changeRegion('global')">Global</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- Shorts Section -->
|
|
|
|
|
|
<!-- Videos Section -->
|
|
<div id="videosSection" class="yt-section">
|
|
<div class="yt-section-header" style="display:none;">
|
|
<h2><i class="fas fa-play-circle"></i> Videos</h2>
|
|
</div>
|
|
<div id="resultsArea" class="yt-video-grid">
|
|
<!-- Initial Skeleton State -->
|
|
<!-- Initial Skeleton State (12 items) -->
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="yt-video-card skeleton-card">
|
|
<div class="skeleton-thumb skeleton"></div>
|
|
<div class="skeleton-details">
|
|
<div class="skeleton-avatar skeleton"></div>
|
|
<div class="skeleton-text">
|
|
<div class="skeleton-title skeleton"></div>
|
|
<div class="skeleton-meta skeleton"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Filter Bar Styles */
|
|
.yt-filter-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
padding: 0 1rem;
|
|
margin-bottom: 1rem;
|
|
position: sticky;
|
|
top: 56px;
|
|
/* Adjust based on header height */
|
|
z-index: 99;
|
|
background: var(--yt-bg-primary);
|
|
border-bottom: 1px solid var(--yt-border);
|
|
}
|
|
|
|
.yt-categories {
|
|
display: flex;
|
|
overflow-x: auto;
|
|
gap: 0.8rem;
|
|
padding: 0.5rem 0;
|
|
flex: 1;
|
|
scrollbar-width: none;
|
|
/* Firefox */
|
|
}
|
|
|
|
.yt-categories::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.yt-chip {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 8px;
|
|
background: var(--yt-bg-secondary);
|
|
color: var(--yt-text-primary);
|
|
border: none;
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
font-size: 0.9rem;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.yt-chip:hover {
|
|
background: var(--yt-bg-hover);
|
|
}
|
|
|
|
.yt-chip-active {
|
|
background: var(--yt-text-primary);
|
|
color: var(--yt-bg-primary);
|
|
}
|
|
|
|
.yt-chip-active:hover {
|
|
background: var(--yt-text-primary);
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.yt-filter-actions {
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
|
|
.yt-dropdown-menu {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
width: 200px;
|
|
background: var(--yt-bg-secondary);
|
|
border-radius: 12px;
|
|
padding: 1rem;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
margin-top: 0.5rem;
|
|
z-index: 100;
|
|
border: 1px solid var(--yt-border);
|
|
}
|
|
|
|
.yt-dropdown-menu.show {
|
|
display: block;
|
|
}
|
|
|
|
.yt-menu-section {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.yt-menu-section:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.yt-menu-section h4 {
|
|
font-size: 0.8rem;
|
|
color: var(--yt-text-secondary);
|
|
margin-bottom: 0.5rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.yt-menu-section button {
|
|
display: block;
|
|
width: 100%;
|
|
text-align: left;
|
|
padding: 0.5rem;
|
|
background: none;
|
|
border: none;
|
|
color: var(--yt-text-primary);
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.yt-menu-section button:hover {
|
|
background: var(--yt-bg-hover);
|
|
}
|
|
|
|
/* Shorts Section Styles */
|
|
.yt-section {
|
|
margin-bottom: 32px;
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.yt-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.yt-section-header h2 {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.yt-section-header h2 i {
|
|
color: var(--yt-accent-red);
|
|
}
|
|
|
|
.yt-shorts-container {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.yt-shorts-arrow {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: var(--yt-bg-primary);
|
|
border: 1px solid var(--yt-border);
|
|
color: var(--yt-text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
z-index: 10;
|
|
transition: all 0.2s;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.yt-shorts-arrow:hover {
|
|
background: var(--yt-bg-secondary);
|
|
transform: translateY(-50%) scale(1.1);
|
|
}
|
|
|
|
.yt-shorts-left {
|
|
left: -20px;
|
|
}
|
|
|
|
.yt-shorts-right {
|
|
right: -20px;
|
|
}
|
|
|
|
.yt-shorts-grid {
|
|
display: flex;
|
|
gap: 12px;
|
|
overflow-x: auto;
|
|
padding: 8px 0;
|
|
scroll-behavior: smooth;
|
|
scrollbar-width: none;
|
|
flex: 1;
|
|
}
|
|
|
|
.yt-shorts-grid::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.yt-short-card {
|
|
flex-shrink: 0;
|
|
width: 180px;
|
|
cursor: pointer;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.yt-short-card:hover {
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.yt-short-thumb {
|
|
width: 180px;
|
|
height: 320px;
|
|
border-radius: 12px;
|
|
object-fit: cover;
|
|
background: var(--yt-bg-secondary);
|
|
opacity: 0;
|
|
transition: opacity 0.5s ease;
|
|
}
|
|
|
|
.yt-short-thumb.loaded {
|
|
opacity: 1;
|
|
|
|
.yt-short-title {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
margin-top: 8px;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.yt-short-views {
|
|
font-size: 12px;
|
|
color: var(--yt-text-secondary);
|
|
margin-top: 4px;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.yt-shorts-arrow {
|
|
display: none;
|
|
}
|
|
|
|
.yt-filter-bar {
|
|
padding: 0 10px;
|
|
top: 56px;
|
|
}
|
|
|
|
.yt-sort-container {
|
|
/* Legacy override if needed */
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
// Global filter state
|
|
let currentSort = 'month';
|
|
let currentRegion = 'vietnam';
|
|
|
|
function toggleFilterMenu() {
|
|
document.getElementById('filterMenu').classList.toggle('show');
|
|
}
|
|
|
|
// Close menu when clicking outside
|
|
document.addEventListener('click', function (e) {
|
|
const menu = document.getElementById('filterMenu');
|
|
const btn = document.getElementById('filterToggleBtn');
|
|
if (menu && btn && !menu.contains(e.target) && !btn.contains(e.target)) {
|
|
menu.classList.remove('show');
|
|
}
|
|
});
|
|
|
|
function changeSort(sort) {
|
|
window.currentSort = sort;
|
|
// Global loadTrending from main.js will use this
|
|
loadTrending(true);
|
|
loadShorts(); // Also reload shorts with new sort
|
|
toggleFilterMenu();
|
|
}
|
|
|
|
function changeRegion(region) {
|
|
window.currentRegion = region;
|
|
loadTrending(true);
|
|
loadShorts(); // Also reload shorts with new region
|
|
toggleFilterMenu();
|
|
}
|
|
|
|
// Scroll shorts logic
|
|
function scrollShorts(direction) {
|
|
const grid = document.getElementById('shortsGrid');
|
|
const scrollAmount = 400;
|
|
|
|
if (direction === 'left') {
|
|
grid.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
|
|
} else {
|
|
grid.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
|
}
|
|
}
|
|
|
|
// Load Shorts Logic
|
|
// Load Shorts Logic
|
|
async function loadShorts() {
|
|
const shortsGrid = document.getElementById('shortsGrid');
|
|
|
|
// Skeleton loader
|
|
shortsGrid.innerHTML = Array(10).fill(0).map(() => `
|
|
<div class="skeleton-short"></div>
|
|
`).join('');
|
|
|
|
try {
|
|
const page = 1;
|
|
const category = window.currentCategory || 'general';
|
|
const shortsCategory = category === 'all' || category === 'general' ? 'shorts' : category;
|
|
|
|
const response = await fetch(`/api/trending?category=${shortsCategory}&page=${page}&sort=${window.currentSort}®ion=${window.currentRegion}&shorts=1`);
|
|
const data = await response.json();
|
|
|
|
shortsGrid.innerHTML = '';
|
|
if (data && data.length > 0) {
|
|
// Show up to 20 shorts
|
|
data.slice(0, 20).forEach(video => {
|
|
const card = document.createElement('div');
|
|
card.className = 'yt-short-card';
|
|
card.innerHTML = `
|
|
<img data-src="${video.thumbnail}" class="yt-short-thumb">
|
|
<p class="yt-short-title">${escapeHtml(video.title)}</p>
|
|
<p class="yt-short-views">${formatViews(video.view_count)} views</p>
|
|
`;
|
|
card.onclick = () => window.location.href = `/watch?v=${video.id}`;
|
|
shortsGrid.appendChild(card);
|
|
});
|
|
if (window.observeImages) window.observeImages();
|
|
} else {
|
|
shortsGrid.innerHTML = '<p style="color:var(--yt-text-secondary);padding:20px;">No shorts found</p>';
|
|
}
|
|
} catch (e) {
|
|
console.error('Error loading shorts:', e);
|
|
shortsGrid.innerHTML = '<p style="color:var(--yt-text-secondary);padding:20px;">Could not load shorts</p>';
|
|
}
|
|
}
|
|
|
|
// Helpers (if main.js not loaded yet or for standalone usage)
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatViews(views) {
|
|
if (!views) return '0';
|
|
const num = parseInt(views);
|
|
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
|
|
if (num >= 1000) return (num / 1000).toFixed(0) + 'K';
|
|
return num.toLocaleString();
|
|
}
|
|
|
|
// Init Logic
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadShorts();
|
|
// Pagination logic removed for infinite scroll
|
|
|
|
// Check URL params for category
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const category = urlParams.get('category');
|
|
if (category && typeof switchCategory === 'function') {
|
|
// Let main.js handle the switch, but we can set UI active state if needed
|
|
// switchCategory is in main.js
|
|
switchCategory(category);
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |