kv-tube/frontend/app/page.tsx

160 lines
5.2 KiB
TypeScript

import Link from 'next/link';
import { cookies } from 'next/headers';
import InfiniteVideoGrid from './components/InfiniteVideoGrid';
import VideoCard from './components/VideoCard';
import {
getSearchVideos,
getHistoryVideos,
getSuggestedVideos,
getRelatedVideos,
getRecentHistory
} from './actions';
import {
VideoData,
CATEGORY_MAP,
ALL_CATEGORY_SECTIONS,
addRegion,
getRandomModifier
} from './utils';
export const dynamic = 'force-dynamic';
const REGION_LABELS: Record<string, string> = {
VN: 'Vietnam',
US: 'United States',
JP: 'Japan',
KR: 'South Korea',
IN: 'India',
GB: 'United Kingdom',
GLOBAL: '',
};
export default async function Home({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}) {
const awaitParams = await searchParams;
const currentCategory = (awaitParams.category as string) || 'All';
const isAllCategory = currentCategory === 'All';
const cookieStore = await cookies();
const regionCode = cookieStore.get('region')?.value || 'VN';
const regionLabel = REGION_LABELS[regionCode] || '';
let gridVideos: VideoData[] = [];
const randomMod = getRandomModifier();
// Fetch recent history for mixing
let recentVideo: VideoData | null = null;
if (isAllCategory) {
recentVideo = await getRecentHistory();
}
if (isAllCategory && recentVideo) {
// 40% Suggested, 40% Related, 20% Trending = 12:12:6 for 30 items
const promises = [
getSuggestedVideos(12),
getRelatedVideos(recentVideo.id, 12),
getSearchVideos(addRegion("trending", regionLabel) + ' ' + randomMod, 6)
];
const [suggestedRes, relatedRes, trendingRes] = await Promise.all(promises);
const interleavedList: VideoData[] = [];
const seenIds = new Set<string>();
let sIdx = 0, rIdx = 0, tIdx = 0;
while (sIdx < suggestedRes.length || rIdx < relatedRes.length || tIdx < trendingRes.length) {
for (let i = 0; i < 2 && sIdx < suggestedRes.length; i++) {
const v = suggestedRes[sIdx++];
if (!seenIds.has(v.id)) { interleavedList.push(v); seenIds.add(v.id); }
}
for (let i = 0; i < 2 && rIdx < relatedRes.length; i++) {
const v = relatedRes[rIdx++];
if (!seenIds.has(v.id)) { interleavedList.push(v); seenIds.add(v.id); }
}
for (let i = 0; i < 1 && tIdx < trendingRes.length; i++) {
const v = trendingRes[tIdx++];
if (!seenIds.has(v.id)) { interleavedList.push(v); seenIds.add(v.id); }
}
}
gridVideos = interleavedList;
} else if (isAllCategory) {
// Fallback if no history
const promises = ALL_CATEGORY_SECTIONS.map(async (sec) => {
return await getSearchVideos(addRegion(sec.query, regionLabel) + ' ' + randomMod, 6);
});
const results = await Promise.all(promises);
// Interleave the results: 1st from Trending, 1st from Music, ... 2nd from Trending, etc.
const maxLen = Math.max(...results.map(arr => arr.length));
const interleavedList: VideoData[] = [];
const seenIds = new Set<string>();
for (let i = 0; i < maxLen; i++) {
for (const categoryResult of results) {
if (i < categoryResult.length) {
const video = categoryResult[i];
if (!seenIds.has(video.id)) {
interleavedList.push(video);
seenIds.add(video.id);
}
}
}
}
gridVideos = interleavedList;
} else if (currentCategory === 'Watched') {
gridVideos = await getHistoryVideos(50);
} else if (currentCategory === 'Suggested') {
gridVideos = await getSuggestedVideos(20);
} else {
const searchQuery = CATEGORY_MAP[currentCategory] || CATEGORY_MAP['All'];
gridVideos = await getSearchVideos(addRegion(searchQuery, regionLabel) + ' ' + randomMod, 30);
}
// Remove duplicates from recent video
if (recentVideo) {
gridVideos = gridVideos.filter(video => video.id !== recentVideo!.id);
}
const categoriesList = Object.keys(CATEGORY_MAP);
return (
<div style={{ paddingTop: '12px' }}>
{/* Category Chips Scrollbar */}
<div style={{ display: 'flex', gap: '12px', padding: '0 12px', marginBottom: '16px', overflowX: 'auto', justifyContent: 'center' }} className="chips-container hide-scrollbox">
{categoriesList.map((cat) => {
const isActive = cat === currentCategory;
return (
<Link key={cat} href={cat === 'All' ? '/' : `/?category=${encodeURIComponent(cat)}`} style={{ textDecoration: 'none' }}>
<button
className={`chip ${isActive ? 'active' : ''}`}
style={{
fontSize: '14px',
whiteSpace: 'nowrap',
transition: 'var(--yt-transition)',
backgroundColor: isActive ? 'var(--foreground)' : 'var(--yt-hover)',
color: isActive ? 'var(--background)' : 'var(--yt-text-primary)'
}}
>
{cat}
</button>
</Link>
);
})}
</div>
<div style={{ padding: '0 12px' }} className="main-container-mobile">
<InfiniteVideoGrid
initialVideos={gridVideos}
currentCategory={currentCategory}
regionLabel={regionLabel}
/>
</div>
</div>
);
}