diff --git a/frontend/src/components/AdminUsers.tsx b/frontend/src/components/AdminUsers.tsx index 720e1c71..09bca40d 100644 --- a/frontend/src/components/AdminUsers.tsx +++ b/frontend/src/components/AdminUsers.tsx @@ -7,7 +7,7 @@ import { notify } from "../lib/notifications"; import { ADMIN_USERS_TAB_KEY } from "../lib/adminUsersTab"; import Tooltip from "./Tooltip"; import { Section } from "./ui/form"; -import { StateMessage } from "./QueryState"; +import { LoadingState, StateMessage } from "./QueryState"; import { useConfirm } from "./ConfirmProvider"; import Tabs, { usePersistedTab } from "./Tabs"; import { PageToolbar } from "./PageShell"; @@ -165,7 +165,7 @@ function UsersRoles({ me }: { me: Me }) { {t("common.loadError")} ) : q.isLoading && !q.data ? ( -
{t("common.loading")}
+{t("users.roles.empty")}
) : ( @@ -319,7 +319,7 @@ function AdminInvites() { {t("common.loadError")} ) : invites.isLoading && !invites.data ? ( -{t("common.loading")}
+{t("settings.invites.noPending")}
) : ( diff --git a/frontend/src/components/AuditLog.tsx b/frontend/src/components/AuditLog.tsx index c4feda96..d413e6fa 100644 --- a/frontend/src/components/AuditLog.tsx +++ b/frontend/src/components/AuditLog.tsx @@ -8,7 +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 { LoadingState, StateMessage } from "./QueryState"; import { auditColumns } from "./auditColumns"; // Admin-only audit trail: an append-only who/what/when log of admin actions + scheduler run @@ -59,7 +59,7 @@ export default function AuditLog() { {t("common.loadError")} ) : q.isLoading ? ( -{t("audit.empty")}
) : ( diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index 124368bd..46c69ad9 100644 --- a/frontend/src/components/ChannelDiscovery.tsx +++ b/frontend/src/components/ChannelDiscovery.tsx @@ -15,7 +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 { LoadingState, StateMessage } from "./QueryState"; import ColumnSortControl from "./ColumnSortControl"; import Pager from "./Pager"; import { PageToolbar } from "./PageShell"; @@ -229,7 +229,7 @@ export default function ChannelDiscovery({ {t("common.loadError")} ) : query.isLoading ? ( -{t("plex.loading")}
+{t("plex.loading")}
+{t("plex.loading")}
+{t("plex.loading")}
+{t("plex.playlist.empty")}
) : ( diff --git a/frontend/src/components/QueryState.tsx b/frontend/src/components/QueryState.tsx index 57f42199..374413e1 100644 --- a/frontend/src/components/QueryState.tsx +++ b/frontend/src/components/QueryState.tsx @@ -1,6 +1,23 @@ import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { AlertTriangle } from "lucide-react"; +import { AlertTriangle, Loader2 } from "lucide-react"; + +// Centered loading state: a spinning accent ring + an optional label. The single shared loading +// visual for a page/section's first load, so all of them read the same (and can be swapped for +// skeletons later in one place). `role=status`/`aria-live` announces it to screen readers. +export function LoadingState({ label, className = "" }: { label?: ReactNode; className?: string }) { + const { t } = useTranslation(); + return ( +