first commit
This commit is contained in:
17
components/admin/AdminHeader.tsx
Normal file
17
components/admin/AdminHeader.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
86
components/admin/AdminSidebar.tsx
Normal file
86
components/admin/AdminSidebar.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
100
components/admin/EpisodeSelector.tsx
Normal file
100
components/admin/EpisodeSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
components/admin/Pagination.tsx
Normal file
49
components/admin/Pagination.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user