mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-20 21:59:53 -05:00
fix: minor fixes in UI
This commit is contained in:
1 parent
715e02e72c
commit
f93bfc7db4
10 files changed
+43
-97
No files matched your search
@@ -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) });
|
||||
}),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }} />
|
||||
</>
|
||||
|
||||
@@ -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 } } },
|
||||
|
||||
Reference in new issue
Block a user