- {sessions.slice(0, 10).map((s) => {
- const isEp = s.series_name && s.season_number != null && s.episode_number != null;
- const title = isEp
- ? s.episode_name || `S${s.season_number}E${s.episode_number}`
- : s.media_title || `File #${s.media_file_id}`;
- const username = s.username || `User #${s.user_id}`;
- return (
-
-
-
-
-
{username}
- {" started watching "}
-
- {title}
-
+ {isLoading ? (
+
+ ) : error ? (
+
+ ) : (
+
+ {sessions.slice(0, 10).map((s) => {
+ const isEp = s.series_name && s.season_number != null && s.episode_number != null;
+ const title = isEp
+ ? s.episode_name || `S${s.season_number}E${s.episode_number}`
+ : s.media_title || `File #${s.media_file_id}`;
+ const username = s.username || `User #${s.user_id}`;
+ return (
+
+
-
- {getTimeAgo(s.started_at)}
+
+
+ {username}
+ {" started watching "}
+
+ {title}
+
+
+
+ {getTimeAgo(s.started_at)}
+
+
+
-
-
- );
- })}
-
+ );
+ })}
+
+ )}
);
@@ -614,3 +700,43 @@ function getTimeAgo(dateStr: string): string {
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
+
+function SectionError({ message }: { message: string }) {
+ return
{message}
;
+}
+
+function LibrarySkeletonRows() {
+ return (
+ <>
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+ >
+ );
+}
+
+function UserSkeletonRows() {
+ return (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+function ActivitySkeletonRows() {
+ return (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ );
+}
diff --git a/web/src/pages/AdminStats.tsx b/web/src/pages/AdminStats.tsx
index 112a7154..db2da80a 100644
--- a/web/src/pages/AdminStats.tsx
+++ b/web/src/pages/AdminStats.tsx
@@ -9,14 +9,14 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
+import { Skeleton } from "@/components/ui/skeleton";
import { Film, FileVideo, Users, Play } from "lucide-react";
+import type { AdminSession, AdminStats } from "@/api/types";
export default function AdminStats() {
- const { data: stats, isLoading: statsLoading } = useAdminStats();
- const { data: sessions = [], isLoading: sessionsLoading } = useAdminSessions();
- const loading = statsLoading || sessionsLoading;
-
- if (loading) return
Loading stats...
;
+ const statsQuery = useAdminStats();
+ const sessionsQuery = useAdminSessions();
+ const sessions = sessionsQuery.data ?? [];
return (
@@ -30,64 +30,107 @@ export default function AdminStats() {
- {stats && (
-
- }
- />
- }
- />
- } />
- }
- />
+
+
+
+
+ );
+}
+
+function StatsCards({
+ stats,
+ sessionCount,
+ isLoading,
+ error,
+}: {
+ stats: AdminStats | undefined;
+ sessionCount: number;
+ isLoading: boolean;
+ error: unknown;
+}) {
+ if (error) {
+ return
;
+ }
+ if (isLoading || !stats) {
+ return (
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+ return (
+
+ } />
+ } />
+ } />
+ } />
+
+ );
+}
+
+function SessionsSection({
+ sessions,
+ isLoading,
+ error,
+}: {
+ sessions: AdminSession[];
+ isLoading: boolean;
+ error: unknown;
+}) {
+ return (
+
+
Active playback sessions
+ {isLoading ? (
+
+ {Array.from({ length: 3 }).map((_, i) => (
+
+ ))}
+
+ ) : error ? (
+
+ ) : sessions.length === 0 ? (
+
+ No active sessions.
+
+ ) : (
+
+
+
+
+ Session ID
+ User ID
+ File ID
+ Method
+ Started
+
+
+
+ {sessions.map((s) => (
+
+ {s.session_id.slice(0, 8)}...
+ {s.user_id}
+ {s.media_file_id}
+ {s.play_method}
+
+ {new Date(s.started_at).toLocaleString()}
+
+
+ ))}
+
+
)}
-
-
-
Active playback sessions
- {sessions.length === 0 ? (
-
- No active sessions.
-
- ) : (
-
-
-
-
- Session ID
- User ID
- File ID
- Method
- Started
-
-
-
- {sessions.map((s) => (
-
-
- {s.session_id.slice(0, 8)}...
-
- {s.user_id}
- {s.media_file_id}
- {s.play_method}
-
- {new Date(s.started_at).toLocaleString()}
-
-
- ))}
-
-
-
- )}
-
);
}
@@ -105,3 +148,11 @@ function StatCard({ title, value, icon }: { title: string; value: number; icon:
);
}
+
+function SectionError({ message }: { message: string }) {
+ return (
+
+ {message}
+
+ );
+}