first commit
This commit is contained in:
84
app/dracin/[id]/page.tsx
Normal file
84
app/dracin/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
33
app/dracin/categories/page.tsx
Normal file
33
app/dracin/categories/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
78
app/dracin/category/[id]/page.tsx
Normal file
78
app/dracin/category/[id]/page.tsx
Normal 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
11
app/dracin/layout.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
export default function DracinLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
5
app/dracin/loading.tsx
Normal file
5
app/dracin/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import LoadingSplash from "@/components/LoadingSplash";
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSplash />;
|
||||
}
|
||||
58
app/dracin/page.tsx
Normal file
58
app/dracin/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
app/dracin/recommend/page.tsx
Normal file
27
app/dracin/recommend/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
34
app/dracin/search/page.tsx
Normal file
34
app/dracin/search/page.tsx
Normal 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
27
app/dracin/vip/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
104
app/dracin/watch/[id]/[episode]/page.tsx
Normal file
104
app/dracin/watch/[id]/[episode]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
5
app/dracin/watch/loading.tsx
Normal file
5
app/dracin/watch/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import LoadingSplash from "@/components/LoadingSplash";
|
||||
|
||||
export default function Loading() {
|
||||
return <LoadingSplash />;
|
||||
}
|
||||
Reference in New Issue
Block a user