mirror of
https://github.com/myronblair/kino-app
synced 2026-06-30 17:50:16 -05:00
123 lines
6.1 KiB
React
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>
|
|
);
|
|
}
|