diff --git a/frontend/src/components/AuditLog.tsx b/frontend/src/components/AuditLog.tsx index 12fb281b..106ccff1 100644 --- a/frontend/src/components/AuditLog.tsx +++ b/frontend/src/components/AuditLog.tsx @@ -8,6 +8,7 @@ import { LS } from "../lib/storage"; import { Section } from "./ui/form"; import { useConfirm } from "./ConfirmProvider"; import DataTable from "./DataTable"; +import { StateMessage } from "./QueryState"; import { auditColumns } from "./auditColumns"; // Admin-only audit trail: an append-only who/what/when log of admin actions + scheduler run @@ -53,7 +54,11 @@ export default function AuditLog() {
{t("audit.intro")}
- {q.isLoading ? ( + {q.isError ? ( +{t("audit.empty")}
diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index 7e9acf1c..74c58945 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -15,6 +15,7 @@ import ChannelLink from "./ChannelLink"; import AboutCell from "./AboutCell"; import DataTable, { type Column } from "./DataTable"; import ChannelLayoutGrid from "./ChannelLayoutGrid"; +import { StateMessage } from "./QueryState"; import ColumnSortControl from "./ColumnSortControl"; import Pager from "./Pager"; import { PageToolbar } from "./PageShell"; @@ -223,7 +224,11 @@ export default function ChannelDiscovery({{t("plex.loading")}
) : items.length === 0 && episodes.length === 0 ? ( dq && plexFilterCount(filters) > 0 ? ( diff --git a/frontend/src/components/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx index 1b525b57..e02ba77b 100644 --- a/frontend/src/components/PlexPlaylistView.tsx +++ b/frontend/src/components/PlexPlaylistView.tsx @@ -33,6 +33,7 @@ import { import { api, type PlexCard } from "../lib/api"; import { getAccountRaw, LS, setAccountRaw } from "../lib/storage"; import { useConfirm } from "./ConfirmProvider"; +import { StateMessage } from "./QueryState"; // A playlist's items, grouped for readability: movies stay standalone, while a run of episodes from the // same show collapses into a season/show group so a whole series doesn't sprawl into an endless flat @@ -576,7 +577,11 @@ export default function PlexPlaylistView({{t("plex.loading")}
) : items.length === 0 ? ({t("plex.playlist.empty")}
diff --git a/frontend/src/components/QueryState.tsx b/frontend/src/components/QueryState.tsx new file mode 100644 index 00000000..4278bb23 --- /dev/null +++ b/frontend/src/components/QueryState.tsx @@ -0,0 +1,48 @@ +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { AlertTriangle } from "lucide-react"; + +// Centered inline status line for a page/section's loading and error(+retry) states, in the house +// style (mirrors Playlists' error block). Text-only by design — R3's goal is to stop pages +// rendering an error or a still-loading fetch AS an "empty" state, not to add skeletons. The +// `role=status`/`aria-live` also gives screen readers the state change (the app had zero live +// regions before). +export function StateMessage({ + children, + onRetry, + tone = "muted", + className = "", +}: { + children: ReactNode; + onRetry?: () => void; + tone?: "muted" | "error"; + className?: string; +}) { + const { t } = useTranslation(); + return ( +