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

View File

@@ -0,0 +1,17 @@
'use client';
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
export default function AdminHeader() {
return (
<header className="hidden md:flex h-16 items-center justify-end gap-4 border-b border-gray-800 bg-[#0a0a0a] px-6">
<div className="flex items-center gap-4">
<span className="text-sm text-gray-400">Admin User</span>
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>AD</AvatarFallback>
</Avatar>
</div>
</header>
)
}

View File

@@ -0,0 +1,86 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { LayoutDashboard, Users, Settings, LogOut, Menu, Shield, Film } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { createClient } from '@/lib/supabase/client';
import { useRouter } from 'next/navigation';
const sidebarItems = [
{ name: 'Dashboard', href: '/admin', icon: LayoutDashboard },
{ name: 'Movies', href: '/admin/movies', icon: Film },
{ name: 'Users', href: '/admin/users', icon: Users },
{ name: 'Roles', href: '/admin/roles', icon: Shield },
{ name: 'Settings', href: '/admin/settings', icon: Settings },
];
export default function AdminSidebar() {
const pathname = usePathname();
const router = useRouter();
const supabase = createClient();
const handleLogout = async () => {
await supabase.auth.signOut();
router.push('/login');
};
const SidebarContent = () => (
<div className="flex flex-col h-full">
<div className="p-6">
<h1 className="text-2xl font-bold text-[#E50914]">CinePrime Admin</h1>
</div>
<nav className="flex-1 px-4 space-y-2">
{sidebarItems.map((item) => {
const isActive = pathname === item.href;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive ? 'bg-primary/10 text-primary' : 'text-gray-400 hover:text-white hover:bg-gray-800'
}`}
>
<item.icon className="w-5 h-5" />
<span className="font-medium">{item.name}</span>
</Link>
);
})}
</nav>
<div className="p-4 border-t border-gray-800">
<Button
variant="ghost"
className="w-full flex justify-start gap-3 text-red-500 hover:text-red-400 hover:bg-red-500/10"
onClick={handleLogout}
>
<LogOut className="w-5 h-5" />
Logout
</Button>
</div>
</div>
);
return (
<>
{/* Desktop Sidebar */}
<aside className="hidden md:flex w-64 flex-col fixed inset-y-0 left-0 bg-[#0a0a0a] border-r border-gray-800 z-50">
<SidebarContent />
</aside>
{/* Mobile Sidebar */}
<div className="md:hidden flex items-center p-4 bg-[#0a0a0a] border-b border-gray-800 sticky top-0 z-50">
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon">
<Menu className="w-6 h-6" />
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0 bg-[#0a0a0a] border-r border-gray-800 text-white w-64">
<SidebarContent />
</SheetContent>
</Sheet>
<span className="ml-4 font-bold text-lg">Admin Panel</span>
</div>
</>
);
}

View File

@@ -0,0 +1,100 @@
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Season } from '@/lib/api';
import { useMemo, useState, useEffect } from 'react';
interface EpisodeSelectorProps {
seasons: Season[];
currentSeason: number;
currentEpisode: number;
}
export function EpisodeSelector({ seasons, currentSeason, currentEpisode }: EpisodeSelectorProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
// Default to first season if currentSeason is 0
const [selectedSeason, setSelectedSeason] = useState<string>(
currentSeason > 0 ? currentSeason.toString() : (seasons[0]?.se.toString() || '1')
);
const activeSeason = useMemo(() =>
seasons.find(s => s.se.toString() === selectedSeason),
[seasons, selectedSeason]
);
const episodes = useMemo(() => {
if (!activeSeason) return [];
return Array.from({ length: activeSeason.maxEp }, (_, i) => i + 1);
}, [activeSeason]);
const handleSeasonChange = (val: string) => {
setSelectedSeason(val);
// When season changes, we don't necessarily update URL until episode is picked?
// Or we can just reset episode logic.
// Let's just update local state, user must pick episode.
};
const handleEpisodeChange = (val: string) => {
const params = new URLSearchParams(searchParams);
params.set('season', selectedSeason);
params.set('episode', val);
router.push(`${pathname}?${params.toString()}`);
};
// Update local state if prop changes (e.g. navigation)
useEffect(() => {
if (currentSeason > 0) {
setSelectedSeason(currentSeason.toString());
}
}, [currentSeason]);
return (
<div className="flex flex-wrap gap-4 p-4 bg-gray-900/50 rounded-lg border border-gray-800">
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">Season</span>
<Select value={selectedSeason} onValueChange={handleSeasonChange}>
<SelectTrigger className="w-[140px] bg-gray-950 border-gray-700">
<SelectValue placeholder="Select Season" />
</SelectTrigger>
<SelectContent>
{seasons.map((season) => (
<SelectItem key={season.se} value={season.se.toString()}>
Season {season.se}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">Episode</span>
<Select
value={currentEpisode > 0 && currentSeason.toString() === selectedSeason ? currentEpisode.toString() : ''}
onValueChange={handleEpisodeChange}
disabled={!activeSeason}
>
<SelectTrigger className="w-[140px] bg-gray-950 border-gray-700">
<SelectValue placeholder="Select Episode" />
</SelectTrigger>
<SelectContent className="max-h-[300px]">
{episodes.map((ep) => (
<SelectItem key={ep} value={ep.toString()}>
Episode {ep}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
);
}

View File

@@ -0,0 +1,49 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface PaginationProps {
currentPage: number;
hasMore: boolean;
}
export function Pagination({ currentPage, hasMore }: PaginationProps) {
const router = useRouter();
const searchParams = useSearchParams();
const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString());
return `?${params.toString()}`;
};
return (
<div className="flex items-center justify-end space-x-2 py-4">
<Button
variant="outline"
size="sm"
onClick={() => router.push(createPageURL(currentPage - 1))}
disabled={currentPage <= 1}
className="bg-transparent border-gray-800 text-white hover:bg-gray-800 hover:text-white"
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<div className="text-sm text-gray-400">
Page {currentPage}
</div>
<Button
variant="outline"
size="sm"
onClick={() => router.push(createPageURL(currentPage + 1))}
disabled={!hasMore}
className="bg-transparent border-gray-800 text-white hover:bg-gray-800 hover:text-white"
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
}