kv-tube/frontend/app/clientActions.ts
2026-03-26 14:16:55 +07:00

271 lines
7.9 KiB
TypeScript

'use client';
import { VideoData } from './constants';
// Use relative URLs - Next.js rewrites will proxy to backend
const API_BASE = '/api';
// Transform backend response to our VideoData format
function transformVideo(item: any): VideoData {
return {
id: item.id || '',
title: item.title || 'Untitled',
thumbnail: item.thumbnail || `https://i.ytimg.com/vi/${item.id}/hqdefault.jpg`,
channelTitle: item.uploader || item.channelTitle || 'Unknown',
channelId: item.channel_id || item.channelId || '',
viewCount: formatViews(item.view_count || 0),
publishedAt: formatRelativeTime(item.upload_date || item.uploaded),
duration: item.duration || '',
description: item.description || '',
uploader: item.uploader,
uploader_id: item.uploader_id,
channel_id: item.channel_id,
view_count: item.view_count || 0,
upload_date: item.upload_date,
};
}
function formatViews(views: number): string {
if (!views) return '0';
if (views >= 1000000000) return (views / 1000000000).toFixed(1) + 'B';
if (views >= 1000000) return (views / 1000000).toFixed(1) + 'M';
if (views >= 1000) return (views / 1000).toFixed(1) + 'K';
return views.toString();
}
function formatRelativeTime(input: any): string {
if (!input) return 'recently';
if (typeof input === 'string' && input.includes('ago')) return input;
const date = new Date(input);
if (isNaN(date.getTime())) return 'recently';
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) return 'today';
if (days === 1) return 'yesterday';
if (days < 7) return `${days} days ago`;
if (days < 30) return `${Math.floor(days / 7)} weeks ago`;
if (days < 365) return `${Math.floor(days / 30)} months ago`;
return `${Math.floor(days / 365)} years ago`;
}
// Search videos using backend API
export async function searchVideosClient(query: string, limit: number = 20): Promise<VideoData[]> {
try {
const response = await fetch(`${API_BASE}/search?q=${encodeURIComponent(query)}&limit=${limit}`, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) return [];
return data.map(transformVideo).filter((v: VideoData) => v.id && v.title);
} catch (error) {
console.error('Search failed:', error);
return [];
}
}
// Get video details using backend API
export async function getVideoDetailsClient(videoId: string): Promise<VideoData | null> {
try {
const response = await fetch(`${API_BASE}/video/${videoId}`, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return transformVideo(data);
} catch (error) {
console.error('Get video details failed:', error);
return null;
}
}
// Get related videos using backend API
export async function getRelatedVideosClient(videoId: string, limit: number = 15): Promise<VideoData[]> {
try {
const response = await fetch(`${API_BASE}/video/${videoId}/related?limit=${limit}`, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) return [];
return data.map(transformVideo).filter((v: VideoData) => v.id && v.title).slice(0, limit);
} catch (error) {
console.error('Get related videos failed:', error);
return [];
}
}
// Get trending videos using backend API with region support
export async function getTrendingVideosClient(regionCode: string = 'US', limit: number = 20): Promise<VideoData[]> {
// Map region codes to search queries for region-specific trending
const regionNames: Record<string, string> = {
'VN': 'Vietnam',
'US': 'United States',
'JP': 'Japan',
'KR': 'South Korea',
'IN': 'India',
'GB': 'United Kingdom',
'DE': 'Germany',
'FR': 'France',
'BR': 'Brazil',
'MX': 'Mexico',
'CA': 'Canada',
'AU': 'Australia',
'GLOBAL': '',
};
const regionName = regionNames[regionCode] || '';
const searchQuery = regionName
? `trending ${regionName} 2026`
: 'trending videos 2026';
try {
const response = await fetch(`${API_BASE}/search?q=${encodeURIComponent(searchQuery)}&limit=${limit}`, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (!Array.isArray(data)) return [];
return data.map(transformVideo).filter((v: VideoData) => v.id && v.title).slice(0, limit);
} catch (error) {
console.error('Get trending videos failed:', error);
return [];
}
}
// Get comments using backend API
export async function getCommentsClient(videoId: string, limit: number = 20): Promise<any[]> {
try {
const response = await fetch(`${API_BASE}/video/${videoId}/comments?limit=${limit}`, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
return [];
}
const data = await response.json();
if (!Array.isArray(data)) return [];
return data.map((c: any) => ({
id: c.id,
text: c.text || c.content,
author: c.author,
authorId: c.author_id,
authorThumbnail: c.author_thumbnail,
likes: c.likes || 0,
published: c.timestamp || 'recently',
isReply: c.is_reply || false,
}));
} catch (error) {
console.error('Get comments failed:', error);
return [];
}
}
// Get channel info using backend API
export async function getChannelInfoClient(channelId: string): Promise<any | null> {
try {
const response = await fetch(`${API_BASE}/channel/info?id=${channelId}`, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
return null;
}
const data = await response.json();
return {
id: data.id || channelId,
title: data.title || 'Unknown Channel',
avatar: data.avatar || '',
banner: data.banner || '',
subscriberCount: data.subscriber_count || 0,
description: data.description || '',
};
} catch (error) {
console.error('Get channel info failed:', error);
return null;
}
}
// Get channel videos using backend API
export async function getChannelVideosClient(channelId: string, limit: number = 30): Promise<VideoData[]> {
try {
const response = await fetch(`${API_BASE}/channel/videos?id=${channelId}&limit=${limit}`, {
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
return [];
}
const data = await response.json();
if (!Array.isArray(data)) return [];
return data.map(transformVideo).filter((v: VideoData) => v.id && v.title);
} catch (error) {
console.error('Get channel videos failed:', error);
return [];
}
}
// Fetch more videos for pagination
export async function fetchMoreVideosClient(
currentCategory: string,
regionLabel: string,
page: number,
contextVideoId?: string
): Promise<VideoData[]> {
const modifiers = ['', 'more', 'new', 'update', 'latest', 'part 2'];
const modifier = page < modifiers.length ? modifiers[page] : `page ${page}`;
let searchQuery = '';
switch (currentCategory) {
case 'All':
case 'Trending':
searchQuery = `trending ${modifier}`;
break;
case 'Music':
searchQuery = `music ${modifier}`;
break;
case 'Gaming':
searchQuery = `gaming ${modifier}`;
break;
case 'News':
searchQuery = `news ${modifier}`;
break;
default:
searchQuery = `${currentCategory.toLowerCase()} ${modifier}`;
}
if (regionLabel && regionLabel !== 'Global') {
searchQuery = `${regionLabel} ${searchQuery}`;
}
return searchVideosClient(searchQuery, 20);
}