fix: minor fixes in UI

This commit is contained in:
Tautvydas Derzinskas committed 2026-09-07 22:24:24 +02:00
1 parent 715e02e72c
commit f93bfc7db4
10 files changed
+43 -97

No files matched your search

+14 -1
View File
@@ -62,12 +62,25 @@ router.post(
asyncHandler(async (req, res) => {
const body = parseBody(loginSchema, req.body);
const email = body.email.toLowerCase();
const user = await prisma.user.findUnique({ where: { email } });
let user = await prisma.user.findUnique({ where: { email } });
// Same generic message whether the email doesn't exist or the password is wrong -- don't
// let a login attempt be used to enumerate registered addresses.
if (!user || !(await bcrypt.compare(body.password, user.passwordHash))) {
throw new HttpError(401, "Invalid email or password");
}
// Mirrors /register's bootstrap exception: if this account registered as the initial-admin
// address before INITIAL_ADMIN_EMAIL was actually visible to the process (a common ordering
// issue -- e.g. a compose .env added after the container was first created), it would be
// stuck as MEMBER forever since role is otherwise only ever set at creation. Promote it here
// too, under the same "only while no admin exists yet" guard.
if (user.role !== "ADMIN" && Boolean(INITIAL_ADMIN_EMAIL) && email === INITIAL_ADMIN_EMAIL) {
const adminExists = (await prisma.user.count({ where: { role: "ADMIN" } })) > 0;
if (!adminExists) {
user = await prisma.user.update({ where: { id: user.id }, data: { role: "ADMIN" } });
}
}
const token = createToken(user.id, user.role);
res.json({ token, expires_in: AUTH_TOKEN_TTL, user: toUserOut(user) });
}),
+6
View File
@@ -5,6 +5,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PrintStash</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap"
rel="stylesheet"
/>
</head>
<body style="margin:0">
<div id="root"></div>
@@ -102,7 +102,6 @@ export default function AppLayout({
<TopBar
title={t(VIEW_TITLE_KEYS[activeView])}
onBack={activeView === "settings" ? onBack : undefined}
showAddMenu={activeView === "library"}
folderId={folderId}
makerworldCookie={makerworldCookie}
thingiverseCookie={thingiverseCookie}
+7 -11
View File
@@ -19,7 +19,6 @@ type Props = {
* nothing currently populates it, but the header supports it the same way it will once a
* page needs one. */
actions?: React.ReactNode;
showAddMenu: boolean;
folderId: string | null;
makerworldCookie: string;
thingiverseCookie: string;
@@ -38,7 +37,6 @@ export default function TopBar({
title,
onBack,
actions,
showAddMenu,
folderId,
makerworldCookie,
thingiverseCookie,
@@ -65,15 +63,13 @@ export default function TopBar({
{actions}
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
{showAddMenu && (
<AddMenu
folderId={folderId}
makerworldCookie={makerworldCookie}
thingiverseCookie={thingiverseCookie}
onUploaded={onUploaded}
onUnauthorized={onUnauthorized}
/>
)}
<AddMenu
folderId={folderId}
makerworldCookie={makerworldCookie}
thingiverseCookie={thingiverseCookie}
onUploaded={onUploaded}
onUnauthorized={onUnauthorized}
/>
<NotificationBell />
<UserMenu
user={user}
+2 -1
View File
@@ -1,5 +1,6 @@
import React from "react";
import Box from "@mui/material/Box";
import { DISPLAY_FONT_STACK } from "../theme";
type Props = {
size?: "lg" | "md" | "sm";
@@ -42,7 +43,7 @@ export default function Wordmark({ size = "md" }: Props) {
component="span"
sx={{
fontSize,
fontFamily: '"PrintStash Display", sans-serif',
fontFamily: DISPLAY_FONT_STACK,
letterSpacing: "0.01em",
color: "text.primary",
}}
-5
View File
@@ -35,16 +35,11 @@
"libraryTitle": "Library",
"newFolder": "New",
"newFolderTitle": "New folder",
"searchPlaceholder": "Search folders",
"searchAriaLabel": "Search folders",
"clearSearchAria": "Clear folder search",
"allFiles": "All files",
"root": "ROOT",
"foldersHeading": "Folders",
"noFoldersYet": "No folders yet",
"createFirstFolder": "Create your first folder",
"noMatchingFolders": "No matching folders",
"clearSearch": "Clear search",
"actionsFor": "Actions for {{name}}",
"collapse": "Collapse",
"expand": "Expand",
-5
View File
@@ -35,16 +35,11 @@
"libraryTitle": "Biblioteka",
"newFolder": "Naujas",
"newFolderTitle": "Naujas aplankas",
"searchPlaceholder": "Ieškoti aplankų",
"searchAriaLabel": "Ieškoti aplankų",
"clearSearchAria": "Išvalyti aplankų paiešką",
"allFiles": "Visi failai",
"root": "ŠAKNIS",
"foldersHeading": "Aplankai",
"noFoldersYet": "Kol kas nėra aplankų",
"createFirstFolder": "Sukurti pirmą aplanką",
"noMatchingFolders": "Atitinkančių aplankų nerasta",
"clearSearch": "Išvalyti paiešką",
"actionsFor": "Veiksmai su {{name}}",
"collapse": "Suskleisti",
"expand": "Išskleisti",
@@ -17,11 +17,9 @@ import type { Folder } from "../../../api/folders";
* function of its props. */
export type FolderTreeContext = {
childrenMap: Record<string, Folder[]>;
visibleFolderIds: Set<string> | null;
expanded: Set<string>;
dropTargetId: string | null;
selectedId: string | null | undefined;
query: string;
untitledLabel: string;
collapseLabel: string;
expandLabel: string;
@@ -43,13 +41,10 @@ type Props = {
/** One row of the recursive folder tree, plus (via a nested Collapse + List) its own
* children rendered as more FolderTreeRow instances. */
export default function FolderTreeRow({ folder, depth, ctx }: Props) {
const { visibleFolderIds, childrenMap, expanded, dropTargetId, selectedId, query, untitledLabel } = ctx;
if (visibleFolderIds && !visibleFolderIds.has(folder.id)) return null;
const children = (childrenMap[folder.id] || []).filter(
child => !visibleFolderIds || visibleFolderIds.has(child.id)
);
const { childrenMap, expanded, dropTargetId, selectedId, untitledLabel } = ctx;
const children = childrenMap[folder.id] || [];
const isSelected = selectedId === folder.id;
const isOpen = !!query.trim() || expanded.has(folder.id);
const isOpen = expanded.has(folder.id);
const isDropTarget = dropTargetId === folder.id;
return (
@@ -5,8 +5,6 @@ import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemIcon from "@mui/material/ListItemIcon";
@@ -16,8 +14,6 @@ import Divider from "@mui/material/Divider";
import Collapse from "@mui/material/Collapse";
import Tooltip from "@mui/material/Tooltip";
import AddIcon from "@mui/icons-material/Add";
import SearchIcon from "@mui/icons-material/Search";
import CloseIcon from "@mui/icons-material/Close";
import CreateNewFolderIcon from "@mui/icons-material/CreateNewFolder";
import EditIcon from "@mui/icons-material/Edit";
import LayersIcon from "@mui/icons-material/Layers";
@@ -129,7 +125,6 @@ export default function Sidebar({
const [editParent, setEditParent] = useState<string | null>(null);
const [dropTargetId, setDropTargetId] = useState<string | null>(null);
const [dropUploading, setDropUploading] = useState(false);
const [query, setQuery] = useState("");
const [menuState, setMenuState] = useState<{ folderId: string; anchorEl: HTMLElement } | null>(null);
const zipPrompt = useZipImportPrompt();
const importModePrompt = useImportModePrompt();
@@ -452,24 +447,6 @@ export default function Sidebar({
return map;
}, [folders]);
const visibleFolderIds = React.useMemo(() => {
const normalizedQuery = query.trim().toLocaleLowerCase();
if (!normalizedQuery) return null;
const visible = new Set<string>();
folders.forEach(folder => {
if (!folderPath(folder).toLocaleLowerCase().includes(normalizedQuery)) return;
let current: Folder | undefined = folder;
const guard = new Set<string>();
while (current && !guard.has(current.id)) {
visible.add(current.id);
guard.add(current.id);
current = current.parent_id ? folderById[current.parent_id] : undefined;
}
});
return visible;
}, [folderById, folderPath, folders, query]);
// Default-expand root folders so they are visible
useEffect(() => {
const roots = childrenMap["__root"] || [];
@@ -494,11 +471,9 @@ export default function Sidebar({
const treeCtx: FolderTreeContext = {
childrenMap,
visibleFolderIds,
expanded,
dropTargetId,
selectedId,
query,
untitledLabel,
collapseLabel: t("sidebar.collapse"),
expandLabel: t("sidebar.expand"),
@@ -587,8 +562,8 @@ export default function Sidebar({
{!collapsed && (
<>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ px: 1, pt: 1, pb: 1 }}>
<Typography variant="h6" noWrap>
{t("sidebar.foldersHeadingWithCount", { count: visibleFolderIds ? visibleFolderIds.size : folders.length })}
<Typography variant="h6" noWrap sx={{ fontFamily: "unset" }}>
{t("sidebar.foldersHeadingWithCount", { count: folders.length })}
</Typography>
<Button
size="small"
@@ -601,35 +576,6 @@ export default function Sidebar({
</Button>
</Stack>
<Box sx={{ px: 1, pb: 1 }}>
<TextField
fullWidth
size="small"
value={query}
onChange={event => setQuery(event.target.value)}
placeholder={t("sidebar.searchPlaceholder") ?? undefined}
inputProps={{ "aria-label": t("sidebar.searchAriaLabel") ?? undefined }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
endAdornment: query ? (
<InputAdornment position="end">
<IconButton
size="small"
onClick={() => setQuery("")}
aria-label={t("sidebar.clearSearchAria") ?? undefined}
>
<CloseIcon fontSize="small" />
</IconButton>
</InputAdornment>
) : undefined,
}}
/>
</Box>
<List disablePadding>
{(childrenMap["__root"] || []).map(f => (
<FolderTreeRow key={f.id} folder={f} depth={0} ctx={treeCtx} />
@@ -642,13 +588,6 @@ export default function Sidebar({
<Button size="small" onClick={() => startCreate(null)}>{t("sidebar.createFirstFolder")}</Button>
</Stack>
)}
{!!folders.length && visibleFolderIds?.size === 0 && (
<Stack alignItems="center" spacing={1} sx={{ py: 4, color: "text.secondary" }}>
<SearchIcon />
<Typography variant="body2">{t("sidebar.noMatchingFolders")}</Typography>
<Button size="small" onClick={() => setQuery("")}>{t("sidebar.clearSearch")}</Button>
</Stack>
)}
<Divider sx={{ my: 1 }} />
</>
+9 -2
View File
@@ -5,6 +5,12 @@ export type { ResolvedTheme };
export const THEME_IDS: ResolvedTheme[] = ["light", "dark"];
const BODY_FONT_STACK =
'"Open Sans", "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif';
/** The brand/display face used for h5/h6 (page and section titles) and the Wordmark -- kept as
* one exported constant so both stay in sync. */
export const DISPLAY_FONT_STACK = '"PrintStash Display", sans-serif';
/**
* Extra design tokens MUI's Theme doesn't model natively: the page background (a plain color
* for light/dark, a layered radial-gradient CSS value for the neon/purple/blue themes) and the
@@ -112,9 +118,10 @@ export function buildTheme(id: ResolvedTheme): Theme {
shape: { borderRadius: 10 },
typography: {
fontSize: 13,
fontFamily: BODY_FONT_STACK,
button: { textTransform: "none", fontWeight: 600 },
h5: { fontFamily: '"PrintStash Display", sans-serif' },
h6: { fontFamily: '"PrintStash Display", sans-serif' },
h5: { fontFamily: DISPLAY_FONT_STACK },
h6: { fontFamily: DISPLAY_FONT_STACK },
},
components: {
MuiPaper: { styleOverrides: { root: { backgroundImage: "none", backgroundColor: d.panel } } },