diff --git a/README.md b/README.md
index 07a60fc..9ffedcb 100644
--- a/README.md
+++ b/README.md
@@ -50,24 +50,24 @@ Instead of having your collection scattered across different websites and your f
-
Dashboard
-
Models
-
Model Details
+
Dashboard
+
Models
+
Model Details
-
3D Preview
-
Collections
-
Tags
+
3D Preview
+
Collections
+
Tags
-
Downloads
-
My Models
-
Dark Theme
+
Downloads
+
My Models
+
Dark Theme
-
Thingport Grab — Printables
-
Thingport Grab — Thingiverse
-
Thingport Grab — MakerWorld
+
Thingport Grab — Printables
+
Thingport Grab — Thingiverse
+
Thingport Grab — MakerWorld
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index cce0384..9c963f9 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -21,6 +21,7 @@ import ProfilePage from "./pages/ProfilePage";
import ChangeEmailPage from "./pages/ProfilePage/ChangeEmailPage";
import ChangePasswordPage from "./pages/ProfilePage/ChangePasswordPage";
import DownloadPage from "./pages/DownloadPage";
+import AdminPage from "./pages/AdminPage";
import AdminSettingsPage from "./pages/AdminSettingsPage";
import UsersPage from "./pages/UsersPage";
import LogsPage from "./pages/LogsPage";
@@ -181,6 +182,10 @@ function AppShell({
element={}
/>
} />
+ : }
+ />
navigate("/");
+ } else if (path === "/admin") {
+ // The Administration hub itself -- a top-level page reachable directly from the sidebar,
+ // same as Models/Collections/Tags/Downloads, so its back button goes to the Dashboard.
+ title = t("sidebar.administration");
+ onBack = () => navigate("/");
} else if (path.startsWith("/admin-settings")) {
title = t("adminSettings.pageTitle");
- onBack = () => navigate("/");
+ onBack = () => navigate("/admin");
} else if (path.startsWith("/admin-users")) {
title = t("adminSettings.users.heading");
- onBack = () => navigate("/");
+ onBack = () => navigate("/admin");
} else if (path.startsWith("/admin-logs")) {
title = t("adminSettings.logs.heading");
- onBack = () => navigate("/");
+ onBack = () => navigate("/admin");
} else if (path.startsWith("/admin-triggers")) {
title = t("adminSettings.triggers.heading");
- onBack = () => navigate("/");
+ onBack = () => navigate("/admin");
} else if (path.startsWith("/admin-connections")) {
title = t("adminSettings.connections.heading");
- onBack = () => navigate("/");
+ onBack = () => navigate("/admin");
}
return { title, onBack };
diff --git a/frontend/src/components/Layout/Sidebar/index.tsx b/frontend/src/components/Layout/Sidebar/index.tsx
index f909b81..f65c094 100644
--- a/frontend/src/components/Layout/Sidebar/index.tsx
+++ b/frontend/src/components/Layout/Sidebar/index.tsx
@@ -12,7 +12,6 @@ import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText from "@mui/material/ListItemText";
-import Collapse from "@mui/material/Collapse";
import Divider from "@mui/material/Divider";
import Tooltip from "@mui/material/Tooltip";
import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard";
@@ -22,13 +21,6 @@ import LocalOfferIcon from "@mui/icons-material/LocalOffer";
import BookmarkIcon from "@mui/icons-material/Bookmark";
import DownloadIcon from "@mui/icons-material/Download";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
-import SettingsIcon from "@mui/icons-material/Settings";
-import PeopleIcon from "@mui/icons-material/People";
-import HistoryIcon from "@mui/icons-material/History";
-import BoltIcon from "@mui/icons-material/Bolt";
-import CableIcon from "@mui/icons-material/Cable";
-import ExpandLessIcon from "@mui/icons-material/ExpandLess";
-import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import Wordmark from "../../Wordmark";
@@ -100,8 +92,9 @@ type Props = {
/** The persistent app-wide navigation rail: Dashboard, Models, Collections, Tags, Downloads, then
* (once any tag is bookmarked) a divider, a "Bookmarks" heading, and one row per bookmarked tag,
- * and finally (for admins) Administration. Category browsing lives inside the Models page
- * itself, not here. */
+ * and finally (for admins) a single "Administration" row -- it's just a link to the /admin hub
+ * page now, not an expandable list of every admin sub-page (see AdminPage). Category browsing
+ * lives inside the Models page itself, not here. */
export default function Sidebar({ isAdmin, onSelectCategory, tagBookmarksVersion }: Props) {
const { t } = useTranslation(["app", "common"]);
const location = useLocation();
@@ -110,7 +103,6 @@ export default function Sidebar({ isAdmin, onSelectCategory, tagBookmarksVersion
if (typeof window === "undefined") return false;
return window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY) === "true";
});
- const [adminExpanded, setAdminExpanded] = useState(false);
const [bookmarkedTags, setBookmarkedTags] = useState([]);
useEffect(() => {
@@ -128,13 +120,10 @@ export default function Sidebar({ isAdmin, onSelectCategory, tagBookmarksVersion
const onTags = location.pathname.startsWith("/models/tags");
const onModels = (location.pathname.startsWith("/models") && !onCollections && !onTags) || location.pathname.startsWith("/authors");
const onDownload = location.pathname.startsWith("/downloads");
- const onAdminSettings = location.pathname.startsWith("/admin-settings");
- const onAdminUsers = location.pathname.startsWith("/admin-users");
- const onAdminLogs = location.pathname.startsWith("/admin-logs");
- const onAdminTriggers = location.pathname.startsWith("/admin-triggers");
- const onAdminConnections = location.pathname.startsWith("/admin-connections");
- const onAdmin = onAdminSettings || onAdminUsers || onAdminLogs || onAdminTriggers || onAdminConnections;
- const adminOpen = adminExpanded || onAdmin;
+ // Covers both the hub itself (/admin) and every sub-page (/admin-settings, /admin-users, ...)
+ // as a plain string-prefix match -- they're only ever reached from that hub now, not listed
+ // individually here any more, so one flag is all this row needs.
+ const onAdmin = location.pathname.startsWith("/admin");
const toggleCollapsed = () => {
setCollapsed(prev => {
@@ -344,106 +333,23 @@ export default function Sidebar({ isAdmin, onSelectCategory, tagBookmarksVersion
<>
{collapsed ? (
- <>
- }
- label={t("common:settings")}
- selected={onAdminSettings}
- onClick={() => navigate("/admin-settings")}
- />
- }
- label={t("adminSettings.users.heading")}
- selected={onAdminUsers}
- onClick={() => navigate("/admin-users")}
- />
- }
- label={t("adminSettings.logs.heading")}
- selected={onAdminLogs}
- onClick={() => navigate("/admin-logs")}
- />
- }
- label={t("adminSettings.triggers.heading")}
- selected={onAdminTriggers}
- onClick={() => navigate("/admin-triggers")}
- />
- }
- label={t("adminSettings.connections.heading")}
- selected={onAdminConnections}
- onClick={() => navigate("/admin-connections")}
- />
- >
+ }
+ label={t("sidebar.administration")}
+ selected={onAdmin}
+ onClick={() => navigate("/admin")}
+ />
) : (
- <>
- setAdminExpanded(v => !v)}
- sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onAdmin) }}
- >
-
-
-
-
- {adminOpen ? : }
-
-
-
- navigate("/admin-settings")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminSettings) }}
- >
-
-
-
-
-
- navigate("/admin-users")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminUsers) }}
- >
-
-
-
-
-
- navigate("/admin-logs")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminLogs) }}
- >
-
-
-
-
-
- navigate("/admin-triggers")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminTriggers) }}
- >
-
-
-
-
-
- navigate("/admin-connections")}
- sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminConnections) }}
- >
-
-
-
-
-
-
-
- >
+ navigate("/admin")}
+ sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onAdmin) }}
+ >
+
+
+
+
+
)}
>
)}
diff --git a/frontend/src/pages/AdminPage/index.tsx b/frontend/src/pages/AdminPage/index.tsx
new file mode 100644
index 0000000..ad251b9
--- /dev/null
+++ b/frontend/src/pages/AdminPage/index.tsx
@@ -0,0 +1,63 @@
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import Paper from "@mui/material/Paper";
+import List from "@mui/material/List";
+import ListItemButton from "@mui/material/ListItemButton";
+import ListItemIcon from "@mui/material/ListItemIcon";
+import ListItemText from "@mui/material/ListItemText";
+import ChevronRightIcon from "@mui/icons-material/ChevronRight";
+import SettingsIcon from "@mui/icons-material/Settings";
+import PeopleIcon from "@mui/icons-material/People";
+import HistoryIcon from "@mui/icons-material/History";
+import BoltIcon from "@mui/icons-material/Bolt";
+import CableIcon from "@mui/icons-material/Cable";
+
+type Section = {
+ path: string;
+ icon: React.ReactNode;
+ labelKey: string;
+};
+
+const SECTIONS: Section[] = [
+ { path: "/admin-settings", icon: , labelKey: "common:settings" },
+ { path: "/admin-users", icon: , labelKey: "adminSettings.users.heading" },
+ { path: "/admin-logs", icon: , labelKey: "adminSettings.logs.heading" },
+ { path: "/admin-triggers", icon: , labelKey: "adminSettings.triggers.heading" },
+ { path: "/admin-connections", icon: , labelKey: "adminSettings.connections.heading" },
+];
+
+/** The Administration hub -- the sidebar now links here instead of listing every admin sub-page
+ * itself (see Sidebar's doc comment). Each row just navigates to that sub-page's own route;
+ * those pages' own back buttons return here (see AppLayout's useRouteChrome), and this page's
+ * own back button goes to the Dashboard, same as every other top-level page reachable directly
+ * from the sidebar (Models, Collections, Tags, Downloads). */
+export default function AdminPage() {
+ const { t } = useTranslation(["app", "common"]);
+ const navigate = useNavigate();
+
+ return (
+ (theme.palette.mode === "dark" ? "transparent" : "divider"),
+ }}
+ >
+
+ {SECTIONS.map((section, idx) => (
+ navigate(section.path)}
+ divider={idx < SECTIONS.length - 1}
+ sx={{ py: 1.5 }}
+ >
+ {section.icon}
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/pages/AdminSettingsPage/index.tsx b/frontend/src/pages/AdminSettingsPage/index.tsx
index 15787c2..1d4c1e9 100644
--- a/frontend/src/pages/AdminSettingsPage/index.tsx
+++ b/frontend/src/pages/AdminSettingsPage/index.tsx
@@ -17,9 +17,9 @@ type Props = {
export default function AdminSettingsPage({ onUnauthorized, onPreviewModeChanged }: Props) {
return (
}>
+
-
);
}