feat: admin menu updates

This commit is contained in:
Tautvydas Derzinskas committed 2026-09-13 11:41:58 +02:00
1 parent dd2652db08
commit 77cf0ab69f
6 files changed
+114 -135

No files matched your search

+12 -12
View File
@@ -50,24 +50,24 @@ Instead of having your collection scattered across different websites and your f
<table>
<tr>
<td width="33%"><img src="frontend/src/assets/screenshots/01_dashboard.png" width="100%" alt="Dashboard"><br><sub><b>Dashboard</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/02_models.png" width="100%" alt="Models"><br><sub><b>Models</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/03_model_details.png" width="100%" alt="Model details"><br><sub><b>Model Details</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/01_dashboard.png" target="_blank"><img src="frontend/src/assets/screenshots/01_dashboard.png" width="100%" alt="Dashboard"></a><br><sub><b>Dashboard</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/02_models.png" target="_blank"><img src="frontend/src/assets/screenshots/02_models.png" width="100%" alt="Models"></a><br><sub><b>Models</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/03_model_details.png" target="_blank"><img src="frontend/src/assets/screenshots/03_model_details.png" width="100%" alt="Model details"></a><br><sub><b>Model Details</b></sub></td>
</tr>
<tr>
<td width="33%"><img src="frontend/src/assets/screenshots/04_model_details_3d_preview.png" width="100%" alt="3D preview"><br><sub><b>3D Preview</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/05_collections.png" width="100%" alt="Collections"><br><sub><b>Collections</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/06_tags.png" width="100%" alt="Tags"><br><sub><b>Tags</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/04_model_details_3d_preview.png" target="_blank"><img src="frontend/src/assets/screenshots/04_model_details_3d_preview.png" width="100%" alt="3D preview"></a><br><sub><b>3D Preview</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/05_collections.png" target="_blank"><img src="frontend/src/assets/screenshots/05_collections.png" width="100%" alt="Collections"></a><br><sub><b>Collections</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/06_tags.png" target="_blank"><img src="frontend/src/assets/screenshots/06_tags.png" width="100%" alt="Tags"></a><br><sub><b>Tags</b></sub></td>
</tr>
<tr>
<td width="33%"><img src="frontend/src/assets/screenshots/07_downloads.png" width="100%" alt="Downloads"><br><sub><b>Downloads</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/08_my_models.png" width="100%" alt="My models"><br><sub><b>My Models</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/09_dark_theme.png" width="100%" alt="Dark theme"><br><sub><b>Dark Theme</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/07_downloads.png" target="_blank"><img src="frontend/src/assets/screenshots/07_downloads.png" width="100%" alt="Downloads"></a><br><sub><b>Downloads</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/08_my_models.png" target="_blank"><img src="frontend/src/assets/screenshots/08_my_models.png" width="100%" alt="My models"></a><br><sub><b>My Models</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/09_dark_theme.png" target="_blank"><img src="frontend/src/assets/screenshots/09_dark_theme.png" width="100%" alt="Dark theme"></a><br><sub><b>Dark Theme</b></sub></td>
</tr>
<tr>
<td width="33%"><img src="frontend/src/assets/screenshots/10_extension_printables.png" width="100%" alt="Thingport Grab on Printables"><br><sub><b>Thingport Grab — Printables</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/11_extension_thingyverse.png" width="100%" alt="Thingport Grab on Thingiverse"><br><sub><b>Thingport Grab — Thingiverse</b></sub></td>
<td width="33%"><img src="frontend/src/assets/screenshots/12_extension_makerworld.png" width="100%" alt="Thingport Grab on MakerWorld"><br><sub><b>Thingport Grab — MakerWorld</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/10_extension_printables.png" target="_blank"><img src="frontend/src/assets/screenshots/10_extension_printables.png" width="100%" alt="Thingport Grab on Printables"></a><br><sub><b>Thingport Grab — Printables</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/11_extension_thingyverse.png" target="_blank"><img src="frontend/src/assets/screenshots/11_extension_thingyverse.png" width="100%" alt="Thingport Grab on Thingiverse"></a><br><sub><b>Thingport Grab — Thingiverse</b></sub></td>
<td width="33%"><a href="frontend/src/assets/screenshots/12_extension_makerworld.png" target="_blank"><img src="frontend/src/assets/screenshots/12_extension_makerworld.png" width="100%" alt="Thingport Grab on MakerWorld"></a><br><sub><b>Thingport Grab — MakerWorld</b></sub></td>
</tr>
</table>
+5
View File
@@ -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={<ChangePasswordPage onUnauthorized={onUnauthorized} />}
/>
<Route path="/downloads" element={<DownloadPage />} />
<Route
path="/admin"
element={isAdmin ? <AdminPage /> : <Navigate to="/" replace />}
/>
<Route
path="/admin-settings"
element={
+10 -5
View File
@@ -96,21 +96,26 @@ function useRouteChrome() {
// Overridden by DownloadPage's usePageHeader once translations resolve.
title = t("sidebar.downloads");
onBack = () => 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 };
+23 -117
View File
@@ -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<string[]>([]);
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
<>
<Divider sx={{ my: 1 }} />
{collapsed ? (
<>
<CollapsedNavIcon
icon={<SettingsIcon fontSize="small" />}
label={t("common:settings")}
selected={onAdminSettings}
onClick={() => navigate("/admin-settings")}
/>
<CollapsedNavIcon
icon={<PeopleIcon fontSize="small" />}
label={t("adminSettings.users.heading")}
selected={onAdminUsers}
onClick={() => navigate("/admin-users")}
/>
<CollapsedNavIcon
icon={<HistoryIcon fontSize="small" />}
label={t("adminSettings.logs.heading")}
selected={onAdminLogs}
onClick={() => navigate("/admin-logs")}
/>
<CollapsedNavIcon
icon={<BoltIcon fontSize="small" />}
label={t("adminSettings.triggers.heading")}
selected={onAdminTriggers}
onClick={() => navigate("/admin-triggers")}
/>
<CollapsedNavIcon
icon={<CableIcon fontSize="small" />}
label={t("adminSettings.connections.heading")}
selected={onAdminConnections}
onClick={() => navigate("/admin-connections")}
/>
</>
<CollapsedNavIcon
icon={<AdminPanelSettingsIcon fontSize="small" />}
label={t("sidebar.administration")}
selected={onAdmin}
onClick={() => navigate("/admin")}
/>
) : (
<>
<ListItemButton
selected={false}
onClick={() => setAdminExpanded(v => !v)}
sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onAdmin) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<AdminPanelSettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("sidebar.administration")} primaryTypographyProps={{ variant: "body2" }} />
{adminOpen ? <ExpandLessIcon fontSize="small" /> : <ExpandMoreIcon fontSize="small" />}
</ListItemButton>
<Collapse in={adminOpen}>
<List disablePadding>
<ListItemButton
selected={onAdminSettings}
onClick={() => navigate("/admin-settings")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminSettings) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<SettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("common:settings")} primaryTypographyProps={{ variant: "body2" }} />
</ListItemButton>
<ListItemButton
selected={onAdminUsers}
onClick={() => navigate("/admin-users")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminUsers) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<PeopleIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("adminSettings.users.heading")} primaryTypographyProps={{ variant: "body2" }} />
</ListItemButton>
<ListItemButton
selected={onAdminLogs}
onClick={() => navigate("/admin-logs")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminLogs) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<HistoryIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("adminSettings.logs.heading")} primaryTypographyProps={{ variant: "body2" }} />
</ListItemButton>
<ListItemButton
selected={onAdminTriggers}
onClick={() => navigate("/admin-triggers")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminTriggers) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<BoltIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("adminSettings.triggers.heading")} primaryTypographyProps={{ variant: "body2" }} />
</ListItemButton>
<ListItemButton
selected={onAdminConnections}
onClick={() => navigate("/admin-connections")}
sx={{ borderRadius: 1, mb: 0.5, pl: 4, ...navRowSx(onAdminConnections) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<CableIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("adminSettings.connections.heading")} primaryTypographyProps={{ variant: "body2" }} />
</ListItemButton>
</List>
</Collapse>
</>
<ListItemButton
selected={onAdmin}
onClick={() => navigate("/admin")}
sx={{ borderRadius: 1, mb: 0.5, ...navRowSx(onAdmin) }}
>
<ListItemIcon sx={{ minWidth: 30 }}>
<AdminPanelSettingsIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary={t("sidebar.administration")} primaryTypographyProps={{ variant: "body2" }} />
</ListItemButton>
)}
</>
)}
+63
View File
@@ -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: <SettingsIcon fontSize="small" />, labelKey: "common:settings" },
{ path: "/admin-users", icon: <PeopleIcon fontSize="small" />, labelKey: "adminSettings.users.heading" },
{ path: "/admin-logs", icon: <HistoryIcon fontSize="small" />, labelKey: "adminSettings.logs.heading" },
{ path: "/admin-triggers", icon: <BoltIcon fontSize="small" />, labelKey: "adminSettings.triggers.heading" },
{ path: "/admin-connections", icon: <CableIcon fontSize="small" />, 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 (
<Paper
variant="outlined"
sx={{
maxWidth: 480,
borderRadius: "12px",
borderColor: (theme) => (theme.palette.mode === "dark" ? "transparent" : "divider"),
}}
>
<List disablePadding>
{SECTIONS.map((section, idx) => (
<ListItemButton
key={section.path}
onClick={() => navigate(section.path)}
divider={idx < SECTIONS.length - 1}
sx={{ py: 1.5 }}
>
<ListItemIcon sx={{ minWidth: 36 }}>{section.icon}</ListItemIcon>
<ListItemText primary={t(section.labelKey)} primaryTypographyProps={{ variant: "body2", fontWeight: 600 }} />
<ChevronRightIcon fontSize="small" sx={{ color: "text.disabled" }} />
</ListItemButton>
))}
</List>
</Paper>
);
}
@@ -17,9 +17,9 @@ type Props = {
export default function AdminSettingsPage({ onUnauthorized, onPreviewModeChanged }: Props) {
return (
<Stack spacing={4} divider={<Divider />}>
<ThingiverseSection onUnauthorized={onUnauthorized} />
<StorageSection onUnauthorized={onUnauthorized} />
<PreviewsSection onUnauthorized={onUnauthorized} onSaved={onPreviewModeChanged} />
<ThingiverseSection onUnauthorized={onUnauthorized} />
</Stack>
);
}