kv-app/app/trending/page.tsx

417 lines
No EOL
15 KiB
TypeScript

'use client'
import { useState } from 'react'
import Header from '@/components/Header'
import { RefreshCw, Loader2, Play, Download, Eye, MessageCircle, Heart, Share2, ExternalLink, AlertTriangle } from 'lucide-react'
import { useSettings } from '@/lib/context/SettingsContext'
import { analyzeMediaWithAI } from '@/lib/api/opencode'
import { generateVideo, waitForTaskCompletion } from '@/lib/api/api34ai'
interface TikTokVideo {
id: string
url: string
thumbnail: string
caption: string
playCount: number
likeCount: number
commentCount: number
shareCount: number
authorName: string
authorAvatar: string
videoUrl?: string
}
const demoVideos: TikTokVideo[] = [
{
id: '1',
url: 'https://www.tiktok.com/@user1/video/123',
thumbnail: 'https://picsum.photos/seed/tiktok1/360/640',
caption: 'Thử thách 24 giờ chỉ ăn một món! 🍜 #foodchallenge #vietnam',
playCount: 2500000,
likeCount: 450000,
commentCount: 12000,
shareCount: 25000,
authorName: 'foodie.vn',
authorAvatar: 'https://api.dicebear.com/7.x/initials/svg?seed=foodie',
},
{
id: '2',
url: 'https://www.tiktok.com/@user2/video/456',
thumbnail: 'https://picsum.photos/seed/tiktok2/360/640',
caption: 'Mua đồ công nghệ giá rẻ ở đâu? 🛒 #tech #shopping',
playCount: 1800000,
likeCount: 320000,
commentCount: 8000,
shareCount: 18000,
authorName: 'techreview.vn',
authorAvatar: 'https://api.dicebear.com/7.x/initials/svg?seed=tech',
},
{
id: '3',
url: 'https://www.tiktok.com/@user3/video/789',
thumbnail: 'https://picsum.photos/seed/tiktok3/360/640',
caption: 'Hướng dẫn make-up tự nhiên cho người mới 💄 #beauty #tutorial',
playCount: 3200000,
likeCount: 680000,
commentCount: 15000,
shareCount: 42000,
authorName: 'beautyqueen',
authorAvatar: 'https://api.dicebear.com/7.x/initials/svg?seed=beauty',
},
{
id: '4',
url: 'https://www.tiktok.com/@user4/video/101',
thumbnail: 'https://picsum.photos/seed/tiktok4/360/640',
caption: 'Phong cách thời trang mùa hè 2024 👗 #fashion #style',
playCount: 2100000,
likeCount: 390000,
commentCount: 9500,
shareCount: 21000,
authorName: 'fashionista.vn',
authorAvatar: 'https://api.dicebear.com/7.x/initials/svg?seed=fashion',
},
{
id: '5',
url: 'https://www.tiktok.com/@user5/video/112',
thumbnail: 'https://picsum.photos/seed/tiktok5/360/640',
caption: 'Bí quyết tập gym tại nhà cho người bận rộn 💪 #fitness #workout',
playCount: 1500000,
likeCount: 280000,
commentCount: 6000,
shareCount: 12000,
authorName: 'fitlife.vn',
authorAvatar: 'https://api.dicebear.com/7.x/initials/svg?seed=fitness',
},
{
id: '6',
url: 'https://www.tiktok.com/@user6/video/131',
thumbnail: 'https://picsum.photos/seed/tiktok6/360/640',
caption: 'Du lịch Việt Nam - Đà Lạt mùa hoa lavender 🌸 #travel #dalat',
playCount: 2800000,
likeCount: 520000,
commentCount: 11000,
shareCount: 28000,
authorName: 'travelblog.vn',
authorAvatar: 'https://api.dicebear.com/7.x/initials/svg?seed=travel',
},
]
function formatNumber(num: number): string {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'
if (num >= 1000) return (num / 1000).toFixed(1) + 'K'
return num.toString()
}
export default function TrendingPage() {
const { settings, isMounted } = useSettings()
const [videos, setVideos] = useState<TikTokVideo[]>([])
const [isLoading, setIsLoading] = useState(false)
const [analyzingId, setAnalyzingId] = useState<string | null>(null)
const [generatingId, setGeneratingId] = useState<string | null>(null)
const [analysisResult, setAnalysisResult] = useState<any>(null)
const [generatedVideo, setGeneratedVideo] = useState<string | null>(null)
const [selectedVideo, setSelectedVideo] = useState<TikTokVideo | null>(null)
const [error, setError] = useState<string | null>(null)
if (!isMounted) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-white">Loading...</div>
</div>
)
}
const handleFetchTrending = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch('/api/tiktok', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ region: 'VN' })
})
const result = await response.json()
if (result.error) {
throw new Error(result.error)
}
if (!result.videos || result.videos.length === 0) {
setVideos(demoVideos)
return
}
const formattedVideos: TikTokVideo[] = result.videos.map((item: any) => ({
id: item.id || item.video?.id,
url: item.url || '',
thumbnail: item.thumbnail || '',
caption: item.caption || '',
playCount: item.playCount || 0,
likeCount: item.likeCount || 0,
commentCount: item.commentCount || 0,
shareCount: item.shareCount || 0,
authorName: item.authorName || 'Unknown',
authorAvatar: item.authorAvatar || '',
}))
setVideos(formattedVideos)
} catch (e: any) {
setError(e.message || 'Failed to fetch trending. Using demo data.')
setVideos(demoVideos)
} finally {
setIsLoading(false)
}
}
const handleAnalyze = async (video: TikTokVideo) => {
if (!settings.opencodeApiKey) {
setError('Please configure OpenCode API key in Settings')
return
}
setAnalyzingId(video.id)
setError(null)
setSelectedVideo(video)
try {
const result = await analyzeMediaWithAI(
settings.opencodeApiKey,
settings.opencodeModel,
'video',
video.thumbnail
)
setAnalysisResult(result)
} catch (e: any) {
setError(e.message || 'Failed to analyze video')
} finally {
setAnalyzingId(null)
}
}
const handleRecreate = async () => {
if (!analysisResult || !settings.api34aiKey) return
setIsLoading(true)
setError(null)
try {
const videoResult = await generateVideo(
settings.api34aiKey,
{
prompt: analysisResult.script,
model: settings.videoModel || 'veo3.1',
duration: settings.defaultDuration,
ratio: settings.defaultAspectRatio,
resolution: '720p',
quality: 'fast'
}
)
const videoUrl = await waitForTaskCompletion(
settings.api34aiKey,
videoResult.task_id
)
setGeneratedVideo(videoUrl)
} catch (e: any) {
setError(e.message || 'Failed to generate video')
} finally {
setIsLoading(false)
}
}
const downloadVideo = async () => {
if (!generatedVideo) return
try {
const response = await fetch(generatedVideo)
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `recreated-${Date.now()}.mp4`
link.click()
window.URL.revokeObjectURL(url)
} catch (e) {
console.error('Download failed:', e)
}
}
return (
<div className="min-h-screen bg-background">
<Header />
<main className="pt-20 pb-8 px-4">
<div className="max-w-6xl mx-auto space-y-6">
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-white mb-2">TikTok Trending - Vietnam</h2>
<p className="text-slate-400">Browse trending TikTok videos, analyze and recreate them</p>
</div>
<div className="flex items-center justify-between">
<button
onClick={handleFetchTrending}
disabled={isLoading}
className="flex items-center gap-2 px-6 py-2.5 bg-primary hover:bg-primary-dark disabled:bg-slate-600 rounded-lg text-white font-medium"
>
{isLoading ? <Loader2 className="animate-spin" size={18} /> : <RefreshCw size={18} />}
{isLoading ? 'Loading...' : 'Refresh Trending'}
</button>
<div className="text-slate-400 text-sm">
Region: 🇻🇳 Vietnam
</div>
</div>
{error && (
<div className="bg-red-500/10 border border-red-500 rounded-lg p-4 text-red-400">
{error}
</div>
)}
{videos.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{videos.map((video) => (
<div
key={video.id}
className="bg-surface rounded-xl overflow-hidden border border-slate-700 hover:border-primary transition-colors"
>
<div className="relative aspect-[9/16] bg-slate-800">
<img
src={video.thumbnail}
alt={video.caption}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent" />
<div className="absolute bottom-3 left-3 right-3">
<p className="text-white text-sm line-clamp-2">{video.caption}</p>
</div>
</div>
<div className="p-4 space-y-3">
<div className="flex items-center gap-2">
<img
src={video.authorAvatar}
alt={video.authorName}
className="w-8 h-8 rounded-full"
/>
<span className="text-white font-medium text-sm">@{video.authorName}</span>
</div>
<div className="flex items-center justify-between text-slate-400 text-sm">
<div className="flex items-center gap-1">
<Eye size={14} />
<span>{formatNumber(video.playCount)}</span>
</div>
<div className="flex items-center gap-1">
<Heart size={14} />
<span>{formatNumber(video.likeCount)}</span>
</div>
<div className="flex items-center gap-1">
<MessageCircle size={14} />
<span>{formatNumber(video.commentCount)}</span>
</div>
<div className="flex items-center gap-1">
<Share2 size={14} />
<span>{formatNumber(video.shareCount)}</span>
</div>
</div>
<div className="flex gap-2 pt-2">
<button
onClick={() => handleAnalyze(video)}
disabled={analyzingId === video.id}
className="flex-1 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg text-sm font-medium text-white transition-colors disabled:opacity-50"
>
{analyzingId === video.id ? 'Analyzing...' : '🔍 Analyze'}
</button>
<a
href={video.url}
target="_blank"
rel="noopener noreferrer"
className="py-2 px-3 bg-slate-700 hover:bg-slate-600 rounded-lg text-white"
>
<ExternalLink size={16} />
</a>
</div>
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 text-slate-400">
<p>No trending videos loaded. Click "Refresh Trending" to fetch.</p>
</div>
)}
{selectedVideo && analysisResult && (
<div className="fixed inset-0 bg-black/80 flex items-center justify-center p-4 z-50">
<div className="bg-surface rounded-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white">AI Analysis</h3>
<button
onClick={() => {
setSelectedVideo(null)
setAnalysisResult(null)
setGeneratedVideo(null)
}}
className="text-slate-400 hover:text-white"
>
</button>
</div>
<div className="space-y-4 mb-6">
<div>
<h4 className="text-sm font-medium text-slate-400 mb-1">Content Summary</h4>
<p className="text-slate-300">{analysisResult.contentSummary}</p>
</div>
<div>
<h4 className="text-sm font-medium text-slate-400 mb-1">Script</h4>
<p className="text-slate-300 whitespace-pre-wrap">{analysisResult.script}</p>
</div>
<div>
<h4 className="text-sm font-medium text-slate-400 mb-2">Keyframes</h4>
<div className="space-y-2">
{analysisResult.keyframes?.map((kf: any, i: number) => (
<div key={i} className="flex gap-3 text-sm">
<span className="text-primary font-mono">{kf.time}</span>
<span className="text-slate-300">{kf.description}</span>
</div>
))}
</div>
</div>
</div>
{generatedVideo ? (
<div className="space-y-4">
<video src={generatedVideo} controls className="w-full rounded-lg" />
<button
onClick={downloadVideo}
className="flex items-center gap-2 px-6 py-2.5 bg-green-600 hover:bg-green-700 rounded-lg text-white font-medium w-full justify-center"
>
<Download size={18} />
Download Video
</button>
</div>
) : (
<button
onClick={handleRecreate}
disabled={isLoading}
className="flex items-center gap-2 px-6 py-2.5 bg-green-600 hover:bg-green-700 disabled:bg-slate-600 rounded-lg text-white font-medium w-full justify-center"
>
{isLoading ? <Loader2 className="animate-spin" size={18} /> : <Play size={18} />}
{isLoading ? 'Generating...' : '🎬 Recreate with 34ai'}
</button>
)}
</div>
</div>
)}
</div>
</main>
</div>
)
}