diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4c4c06d..804c01d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -31,6 +31,7 @@ import PageShell from "./components/PageShell"; import GlassTuner from "./components/GlassTuner"; import ChatDock from "./components/ChatDock"; import Toaster from "./components/Toaster"; +import { StateMessage } from "./components/QueryState"; import ErrorDialog from "./components/ErrorDialog"; import VersionBanner from "./components/VersionBanner"; import DemoBanner from "./components/DemoBanner"; @@ -294,8 +295,10 @@ export default function App() { // network/5xx failure, and no data means "not signed in" → the public landing. if (meQuery.error) return ( -
{t("users.roles.intro")}
- {rows.length === 0 ? ( + {q.isError && !q.data ? ( +{t("users.roles.empty")}
) : ({t("settings.invites.noPending")}
) : ({t("audit.intro")}
- {q.isLoading ? ( -{t("audit.empty")}
) : ( diff --git a/frontend/src/components/ChannelDiscovery.tsx b/frontend/src/components/ChannelDiscovery.tsx index 7e9acf1..46c69ad 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 { LoadingState, StateMessage } from "./QueryState"; import ColumnSortControl from "./ColumnSortControl"; import Pager from "./Pager"; import { PageToolbar } from "./PageShell"; @@ -223,8 +224,12 @@ export default function ChannelDiscovery({{t("plex.loading")}
) : items.length === 0 && episodes.length === 0 ? ( dq && plexFilterCount(filters) > 0 ? ( @@ -594,8 +599,12 @@ function PlexInfoView({ {t("plex.backToLibrary")}{t("plex.loading")}
+ {q.isError && !q.data ? ( +{t("plex.loading")}
+ {q.isError && !q.data ? ( +{t("plex.loading")}
+ {q.isError && !q.data ? ( +{t("plex.loading")}
+ {q.isError && !q.data ? ( +{t("plex.playlist.empty")}
) : ( diff --git a/frontend/src/components/QueryState.tsx b/frontend/src/components/QueryState.tsx new file mode 100644 index 0000000..374413e --- /dev/null +++ b/frontend/src/components/QueryState.tsx @@ -0,0 +1,67 @@ +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +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 ( +