Files
kino-app/frontend/src/pages/Admin.jsx
T
2026-04-29 16:01:20 +00:00

123 lines
6.1 KiB
React

import { useEffect, useState } from "react";
import api from "../lib/api";
import { toast } from "sonner";
import { Trash2, Star, Film, Settings as SettingsIcon, Download, RefreshCcw } from "lucide-react";
import { useNavigate, Link } from "react-router-dom";
export default function Admin() {
const nav = useNavigate();
const [movies, setMovies] = useState([]);
const [transcoding, setTranscoding] = useState({});
const load = async () => {
const { data } = await api.get("/movies");
setMovies(data);
};
useEffect(() => { load(); }, []);
// Poll for transcoding status
useEffect(() => {
const inProgress = movies.some((m) => m.hls_status === "running" || m.hls_status === "pending");
if (!inProgress) return;
const t = setInterval(load, 3000);
return () => clearInterval(t);
}, [movies]);
const remove = async (id) => {
if (!window.confirm("Remove this movie permanently?")) return;
await api.delete(`/movies/${id}`);
toast.success("Removed");
load();
};
const toggleFeatured = async (m) => {
await api.patch(`/movies/${m.id}`, { featured: !m.featured });
load();
};
const startTranscode = async (m) => {
setTranscoding({ ...transcoding, [m.id]: true });
try {
await api.post(`/movies/${m.id}/transcode`);
toast.success("Transcoding started — refresh in a few minutes");
load();
} catch (err) {
toast.error(err.response?.data?.detail || "Could not start transcode");
} finally {
setTranscoding({ ...transcoding, [m.id]: false });
}
};
const hlsStatusBadge = (m) => {
if (!m.hls_status) return null;
const colors = {
pending: "text-[#fcd34d]",
running: "text-[#fcd34d]",
done: "text-[#86efac]",
failed: "text-[#fca5a5]",
};
return <span className={`text-[9px] uppercase tracking-[0.2em] ${colors[m.hls_status] || "text-[#8A8A8A]"}`}>HLS {m.hls_status}</span>;
};
return (
<div className="min-h-screen bg-[#050505] pt-32 pb-24" data-testid="admin-page">
<div className="px-6 md:px-12 max-w-[1500px] mx-auto">
<span className="text-xs uppercase tracking-[0.3em] text-[#D9381E]">Library management</span>
<h1 className="font-display text-5xl md:text-6xl font-black tracking-tighter text-white mt-3">Admin</h1>
<p className="text-[#8A8A8A] mt-4">Manage your library, requests, integrations.</p>
<div className="mt-10 flex flex-wrap gap-3">
<Link to="/admin/upload" className="flex items-center gap-2 bg-[#D9381E] hover:bg-[#ED4B32] text-white px-5 py-2 text-xs uppercase tracking-[0.2em]" data-testid="admin-upload-link">
<Film size={14} strokeWidth={1.5} /> Upload Movie
</Link>
<Link to="/admin/radarr" className="flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white px-5 py-2 text-xs uppercase tracking-[0.2em] border border-white/10" data-testid="admin-radarr-link">
<Download size={14} strokeWidth={1.5} /> Radarr Import
</Link>
<Link to="/admin/settings" className="flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white px-5 py-2 text-xs uppercase tracking-[0.2em] border border-white/10" data-testid="admin-settings-link">
<SettingsIcon size={14} strokeWidth={1.5} /> Settings
</Link>
<Link to="/requests" className="flex items-center gap-2 bg-white/10 hover:bg-white/20 text-white px-5 py-2 text-xs uppercase tracking-[0.2em] border border-white/10">
Review Requests
</Link>
</div>
<div className="mt-12 border border-[#222]">
<div className="grid grid-cols-12 px-5 py-3 border-b border-[#222] text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">
<span className="col-span-4">Title</span>
<span className="col-span-1">Year</span>
<span className="col-span-1">Rating</span>
<span className="col-span-2">Source</span>
<span className="col-span-2">HLS</span>
<span className="col-span-2 text-right">Actions</span>
</div>
{movies.map((m) => (
<div key={m.id} className="grid grid-cols-12 items-center px-5 py-4 border-b border-[#222] last:border-b-0 hover:bg-[#0F0F0F] transition-colors" data-testid={`admin-movie-row-${m.id}`}>
<div className="col-span-4 flex items-center gap-3 min-w-0">
<img src={m.poster_url} alt="" className="w-10 h-14 object-cover" />
<span className="text-white truncate">{m.title}</span>
</div>
<span className="col-span-1 text-[#8A8A8A] text-sm">{m.year}</span>
<span className="col-span-1 text-[#8A8A8A] text-sm">{m.rating}</span>
<span className="col-span-2 text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">{m.storage_type}</span>
<span className="col-span-2">{hlsStatusBadge(m)}</span>
<div className="col-span-2 flex justify-end gap-2">
{(m.storage_type === "local" || m.storage_type === "radarr") && m.hls_status !== "done" && m.hls_status !== "running" && (
<button onClick={() => startTranscode(m)} disabled={transcoding[m.id]}
className="text-[10px] uppercase tracking-[0.2em] border border-[#222] hover:border-[#D9381E] hover:text-[#D9381E] text-[#8A8A8A] px-3 py-1 disabled:opacity-50"
data-testid={`transcode-${m.id}`}>HLS</button>
)}
<button onClick={() => toggleFeatured(m)} className={`${m.featured ? "text-[#D9381E]" : "text-[#8A8A8A]"} hover:text-[#ED4B32] transition-colors`} aria-label="Feature" data-testid={`feature-${m.id}`}>
<Star size={16} strokeWidth={1.5} fill={m.featured ? "#D9381E" : "none"} />
</button>
<button onClick={() => remove(m.id)} className="text-[#8A8A8A] hover:text-[#fca5a5] transition-colors" aria-label="Delete" data-testid={`delete-${m.id}`}>
<Trash2 size={16} strokeWidth={1.5} />
</button>
</div>
</div>
))}
</div>
</div>
</div>
);
}