Initial commit

This commit is contained in:
twotalesanimation
2026-06-11 10:46:09 +02:00
commit 81ad7e4ea9
223 changed files with 39530 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
'use client';
import * as React from 'react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { AppSidebar } from '@/components/app-sidebar';
import { SiteHeader } from '@/components/site-header';
import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar';
import { Button } from '@/components/ui/button';
interface WatchHistoryItem {
id: string;
videoId: string;
lastPos?: number;
updatedAt: string;
video: {
id: string;
title: string;
thumbnail?: string;
durationSec?: number;
url: string;
};
}
export default function WatchHistoryClient() {
const router = useRouter();
const [history, setHistory] = React.useState<WatchHistoryItem[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
const fetchHistory = async () => {
try {
const res = await fetch('/api/watch-history');
if (res.ok) {
const data = await res.json();
setHistory(data);
}
} catch (err) {
console.error('Failed to fetch watch history', err);
} finally {
setIsLoading(false);
}
};
fetchHistory();
}, []);
const handleVideoClick = (videoId: string) => {
router.push(`/videoplayer?videoId=${videoId}`);
};
const formatTime = (seconds?: number) => {
if (!seconds) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
if (isLoading) {
return (
<SidebarProvider
style={
{
'--sidebar-width': 'calc(var(--spacing) * 72)',
'--header-height': 'calc(var(--spacing) * 12)',
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8">
<Card>
<CardHeader>
<CardTitle>Watch History</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<p className="text-muted-foreground">Loading...</p>
</div>
</CardContent>
</Card>
</div>
</SidebarInset>
</SidebarProvider>
);
}
return (
<SidebarProvider
style={
{
'--sidebar-width': 'calc(var(--spacing) * 72)',
'--header-height': 'calc(var(--spacing) * 12)',
} as React.CSSProperties
}
>
<AppSidebar variant="inset" />
<SidebarInset>
<SiteHeader />
<div className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8">
<Card>
<CardHeader>
<CardTitle>Watch History</CardTitle>
</CardHeader>
<CardContent>
{history.length === 0 ? (
<div className="flex items-center justify-center py-8">
<p className="text-muted-foreground">No videos watched yet</p>
</div>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Thumbnail</TableHead>
<TableHead>Title</TableHead>
<TableHead>Last Position</TableHead>
<TableHead>Last Watched</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{history.map((item) => (
<TableRow key={item.id}>
<TableCell>
<div
className="cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => handleVideoClick(item.video.id)}
>
{item.video.thumbnail ? (
<div className="relative w-24 h-14">
<Image
src={item.video.thumbnail}
alt={item.video.title}
fill
unoptimized
className="object-cover rounded"
/>
</div>
) : (
<div className="w-24 h-14 bg-muted rounded flex items-center justify-center">
<span className="text-xs text-muted-foreground">
No image
</span>
</div>
)}
</div>
</TableCell>
<TableCell>
<p
className="font-medium max-w-xs truncate cursor-pointer hover:underline"
onClick={() => handleVideoClick(item.video.id)}
>
{item.video.title}
</p>
</TableCell>
<TableCell>
<span className="text-sm">
{formatTime(item.lastPos)} /{' '}
{formatTime(item.video.durationSec)}
</span>
</TableCell>
<TableCell>
<span className="text-sm text-muted-foreground">
{formatDate(item.updatedAt)}
</span>
</TableCell>
<TableCell>
<Button
variant="outline"
size="sm"
onClick={() => handleVideoClick(item.video.id)}
>
Resume
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
</SidebarInset>
</SidebarProvider>
);
}