first commit

This commit is contained in:
gotolombok
2026-01-31 17:21:32 +08:00
commit f839fbd63a
109 changed files with 19204 additions and 0 deletions

84
app/dracin/[id]/page.tsx Normal file
View File

@@ -0,0 +1,84 @@
import { getDracinDetail } from '@/lib/dramabox';
import Image from 'next/image';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { PlayCircle } from 'lucide-react';
import DracinEpisodeList from '@/components/DracinEpisodeList';
import BookmarkButton from '@/components/BookmarkButton';
interface DracinDetailPageProps {
params: Promise<{ id: string }>;
}
export default async function DracinDetailPage({ params }: DracinDetailPageProps) {
const { id } = await params;
const { drama, chapters } = await getDracinDetail(id);
if (!drama) {
return <div className="pt-24 text-center text-white">Drama not found.</div>;
}
return (
<div className="pt-24 px-4 md:px-16 min-h-screen">
<div className="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8">
{/* Cover Image */}
<div className="relative aspect-[2/3] w-full max-w-[300px] mx-auto md:mx-0 rounded-lg overflow-hidden border border-gray-800 shadow-xl">
<Image
src={drama.cover || '/placeholder.png'}
alt={drama.name || 'Drama Cover'}
fill
className="object-cover"
priority
unoptimized
/>
</div>
{/* Details */}
<div className="space-y-6 text-white">
<div>
<div className="flex items-center justify-between mb-2">
<h1 className="text-3xl font-bold">{drama.name}</h1>
<BookmarkButton
id={id}
type="dracin"
title={drama.name}
poster={drama.cover || ''}
/>
</div>
<div className="flex gap-2 mb-4 flex-wrap">
{drama.tags && drama.tags.map(tag => (
<Badge key={tag.tagId} variant="secondary" className="bg-gray-800">{tag.tagName}</Badge>
))}
</div>
{/* Episode List */}
<Card className="bg-[#141414] border-gray-800 text-white mt-4 mb-6">
<CardHeader>
<CardTitle>Episodes</CardTitle>
</CardHeader>
<CardContent>
<DracinEpisodeList chapters={chapters} bookId={id} />
</CardContent>
</Card>
<p className="text-gray-400">{drama.introduction}</p>
</div>
<div className="flex gap-8 text-sm text-gray-400 border-y border-gray-800 py-4">
<div>
<span className="block font-bold text-white mb-1">Chapters</span>
{drama.chapterCount}
</div>
<div>
<span className="block font-bold text-white mb-1">Views</span>
{drama.playCount.toLocaleString()}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { getDracinCategories } from '@/lib/dramabox';
import Link from 'next/link';
export default async function DracinCategoriesPage() {
const categories = await getDracinCategories();
return (
<div className="pt-24 px-4 md:px-16 min-h-screen">
<div className="mb-8">
<h1 className="text-2xl font-bold text-white mb-2">Categories</h1>
<p className="text-gray-400">Browse by category.</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
{categories.length > 0 ? (
categories.map((cat) => (
<Link
key={cat.id}
href={`/dracin/category/${cat.id}`}
className="bg-[#141414] border border-gray-800 hover:border-[#E50914] text-gray-300 hover:text-white p-4 rounded-md transition-colors flex items-center justify-center text-center font-medium"
>
{cat.name}
</Link>
))
) : (
<div className="col-span-full text-center text-gray-500 py-20">
No categories found.
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,78 @@
import { getDracinCategoryDetail } from '@/lib/dramabox';
import DracinCard from '@/components/DracinCard';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { redirect } from 'next/navigation';
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ page?: string }>;
}
export default async function DracinCategoryDetailPage(props: PageProps) {
const params = await props.params;
const searchParams = await props.searchParams;
if (!params.id) redirect('/dracin/categories');
const page = Number(searchParams.page) || 1;
const { books, categoryName, totalPages } = await getDracinCategoryDetail(params.id, page, 20);
return (
<div className="pt-24 px-4 md:px-16 min-h-screen">
<div className="mb-8">
<h1 className="text-2xl font-bold text-white mb-2">{categoryName || `Category ${params.id}`}</h1>
<p className="text-gray-400">Browse dramas in this category.</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{books.length > 0 ? (
books.map((item, idx) => (
<DracinCard key={`${item.id}-${idx}`} item={item} />
))
) : (
<div className="col-span-full text-center text-gray-500 py-20">
No dramas found in this category.
</div>
)}
</div>
{/* Pagination Controls */}
{totalPages > 1 && (
<div className="flex justify-center gap-4 mt-8 pb-8">
<Button
disabled={page <= 1}
variant="outline"
className="bg-zinc-800 text-white border-zinc-700 hover:bg-zinc-700 hover:text-white disabled:opacity-50"
asChild
>
{page > 1 ? (
<Link href={`/dracin/category/${params.id}?page=${page - 1}`}>
Previous
</Link>
) : (
<span>Previous</span>
)}
</Button>
<span className="flex items-center text-white px-4">
Page {page} of {totalPages}
</span>
<Button
disabled={page >= totalPages}
variant="outline"
className="bg-zinc-800 text-white border-zinc-700 hover:bg-zinc-700 hover:text-white disabled:opacity-50"
asChild
>
{page < totalPages ? (
<Link href={`/dracin/category/${params.id}?page=${page + 1}`}>
Next
</Link>
) : (
<span>Next</span>
)}
</Button>
</div>
)}
</div>
);
}

11
app/dracin/layout.tsx Normal file
View File

@@ -0,0 +1,11 @@
export default function DracinLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
{children}
</>
);
}

5
app/dracin/loading.tsx Normal file
View File

@@ -0,0 +1,5 @@
import LoadingSplash from "@/components/LoadingSplash";
export default function Loading() {
return <LoadingSplash />;
}

58
app/dracin/page.tsx Normal file
View File

@@ -0,0 +1,58 @@
import { getDracinHome } from '@/lib/dramabox';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import DracinCard from '@/components/DracinCard';
interface DracinPageProps {
searchParams: Promise<{ page?: string }>;
}
export default async function DracinPage({ searchParams }: DracinPageProps) {
const params = await searchParams;
const page = Number(params.page) || 1;
const size = 24;
const dracinList = await getDracinHome(page, size);
return (
<div className="pt-24 px-4 md:px-16 min-h-screen pb-10">
<h1 className="text-2xl font-bold text-white mb-2">Chinese Drama</h1>
<div className="flex items-center gap-4 text-sm md:text-base text-gray-400 mb-6">
<Link href="/dracin/vip" className="hover:text-red-600 transition">VIP</Link>
<span className="w-1 h-1 bg-gray-600 rounded-full"></span>
<Link href="/dracin/recommend" className="hover:text-red-600 transition">Recommend</Link>
<span className="w-1 h-1 bg-gray-600 rounded-full"></span>
<Link href="/dracin/categories" className="hover:text-red-600 transition">Categories</Link>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 mb-8">
{dracinList.map((item) => (
<DracinCard key={item.id} item={item} />
))}
</div>
{dracinList.length === 0 && (
<div className="text-gray-500 text-center py-10">No dramas found.</div>
)}
{/* Pagination Controls */}
<div className="flex justify-center items-center gap-4 mt-8">
<Link href={page > 1 ? `/dracin?page=${page - 1}` : '#'}>
<Button variant="outline" disabled={page <= 1} className="flex items-center gap-1 bg-black/50 text-white border-gray-700 hover:bg-gray-800">
Previous
</Button>
</Link>
<span className="text-white font-medium">Page {page}</span>
<Link href={dracinList.length === size ? `/dracin?page=${page + 1}` : '#'}>
<Button
variant="outline"
disabled={dracinList.length < size}
className="flex items-center gap-1 bg-black/50 text-white border-gray-700 hover:bg-gray-800"
>
Next
</Button>
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { getDracinRecommend } from '@/lib/dramabox';
import DracinCard from '@/components/DracinCard';
export default async function DracinRecommendPage() {
const items = await getDracinRecommend(1, 40); // Fetch a good amount
return (
<div className="pt-24 px-4 md:px-16 min-h-screen">
<div className="mb-8">
<h1 className="text-2xl font-bold text-white mb-2">Just For You</h1>
<p className="text-gray-400">Chinese dramas for you.</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{items.length > 0 ? (
items.map((item, idx) => (
<DracinCard key={`${item.id}-${idx}`} item={item} />
))
) : (
<div className="col-span-full text-center text-gray-500 py-20">
No recommendations found.
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { searchDracin } from '@/lib/dramabox';
import DracinCard from '@/components/DracinCard';
interface SearchPageProps {
searchParams: Promise<{ q: string }>;
}
export default async function DracinSearchPage({ searchParams }: SearchPageProps) {
const { q } = await searchParams;
const items = q ? await searchDracin(q, 1, 40) : [];
return (
<div className="pt-24 px-4 md:px-16 min-h-screen">
<div className="mb-8">
<h1 className="text-2xl font-bold text-white mb-2">Search Results</h1>
<p className="text-gray-400">
{q ? `Results for "${q}"` : 'Enter a keyword to search'}
</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{items.length > 0 ? (
items.map((item, idx) => (
<DracinCard key={`${item.id}-${idx}`} item={item} />
))
) : (
<div className="col-span-full text-center text-gray-500 py-20">
{q ? 'No results found.' : 'Waiting for input...'}
</div>
)}
</div>
</div>
);
}

27
app/dracin/vip/page.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { getDracinVip } from '@/lib/dramabox';
import DracinCard from '@/components/DracinCard';
export default async function DracinVipPage() {
const items = await getDracinVip(1, 40);
return (
<div className="pt-24 px-4 md:px-16 min-h-screen">
<div className="mb-8">
<h1 className="text-2xl font-bold text-[#f5c518] mb-2">Premium Dramas</h1>
<p className="text-gray-400">Exclusive premium content.</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
{items.length > 0 ? (
items.map((item, idx) => (
<DracinCard key={`${item.id}-${idx}`} item={item} />
))
) : (
<div className="col-span-full text-center text-gray-500 py-20">
No VIP items found.
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,104 @@
import { getDracinStream, getDracinDetail } from '@/lib/dramabox';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { List, ArrowRight, ArrowLeft } from 'lucide-react';
import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';
import VideoPlayer from '@/components/VideoPlayer';
import { DracinPlayer } from '@/components/DracinPlayer';
interface DracinWatchPageProps {
params: Promise<{ id: string; episode: string }>;
}
export default async function DracinWatchPage({ params }: DracinWatchPageProps) {
const { id, episode } = await params;
const episodeNum = parseInt(episode);
// Auth Check
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
redirect('/login');
}
// Fetch stream URL
const streamUrl = await getDracinStream(id, episodeNum);
// Fetch detail for title and navigation context
const { drama } = await getDracinDetail(id);
if (!streamUrl) {
return (
<div className="pt-24 px-4 text-center text-white">
<h1 className="text-2xl font-bold mb-4">Error</h1>
<p className="mb-8">Could not load stream for Episode {episode}. It might be a premium/VIP episode or API error.</p>
<Link href={`/dracin/${id}`}>
<Button variant="outline">Back to Detail</Button>
</Link>
</div>
);
}
return (
<div className="pt-20 min-h-screen bg-black flex flex-col items-center">
{/* Player Container */}
<div className="w-full max-w-5xl aspect-video bg-black relative shadow-2xl">
<DracinPlayer
id={id}
episode={episodeNum}
title={drama?.name || 'Unknown Drama'}
poster={drama?.cover || ''}
streamUrl={streamUrl}
/>
</div>
{/* Info & Navigation */}
<div className="w-full max-w-5xl px-4 py-6 text-white">
<div className="mb-6">
<h1 className="text-xl md:text-2xl font-bold mb-6 text-center">
{drama?.name || 'Unknown Drama'} - Episode {episodeNum + 1}
</h1>
<div className="grid grid-cols-3 items-center">
{/* Left: Previous */}
<div className="flex justify-start">
{episodeNum > 0 ? (
<Link href={`/dracin/watch/${id}/${episodeNum - 1}`}>
<Button variant="outline" className="border-gray-700">
<ArrowLeft className="w-4 h-4 mr-2" />
Previous
</Button>
</Link>
) : (
<div />
)}
</div>
{/* Center: Back */}
<div className="flex justify-center">
<Link href={`/dracin/${id}`}>
<Button variant="ghost" className="bg-[#141414] hover:bg-[#b00710] text-white">
<List className="w-4 h-4 mr-2" />
List
</Button>
</Link>
</div>
{/* Right: Next */}
<div className="flex justify-end">
<Link href={`/dracin/watch/${id}/${episodeNum + 1}`}>
<Button className="bg-[#E50914] hover:bg-[#b00710] text-white">
Next
<ArrowRight className="w-4 h-4 ml-2" />
</Button>
</Link>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,5 @@
import LoadingSplash from "@/components/LoadingSplash";
export default function Loading() {
return <LoadingSplash />;
}