Polish UI, harden company creation, and add error pages
This commit is contained in:
+49
-40
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Box, Button, CssBaseline, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material";
|
||||
import { Box, Button, CssBaseline, Typography } from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { CssVarsProvider } from "@mui/material/styles";
|
||||
|
||||
@@ -38,27 +38,39 @@ import AdminAuditPage from "./pages/AdminAuditPage";
|
||||
import AdminUsersPage from "./pages/AdminUsersPage";
|
||||
import AdminSystemPage from "./pages/AdminSystemPage";
|
||||
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
||||
import NotFoundPage from "./pages/NotFoundPage";
|
||||
import RouteErrorPage from "./pages/RouteErrorPage";
|
||||
import { api } from "./api";
|
||||
import { clearAuthToken, getAuthToken } from "./auth";
|
||||
import AppShell, { NavItem } from "./layout/AppShell";
|
||||
import { clearAccentColor, getAccentColor, getThemeModePref, setAccentColor, setThemeModePref, ThemeModePref } from "./themePrefs";
|
||||
|
||||
type AuthConfig = { requireAuth: boolean };
|
||||
type MeResponse = { provider?: "local" | "google" | "external"; id?: string; email?: string; userName?: string; roles?: string[] };
|
||||
type MeResponse = {
|
||||
provider?: "local" | "google" | "external";
|
||||
id?: string;
|
||||
email?: string;
|
||||
userName?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
displayName?: string;
|
||||
avatarImageDataUrl?: string;
|
||||
roles?: string[];
|
||||
};
|
||||
|
||||
function breadcrumbsFor(path: string, t: (k: any) => string): string[] {
|
||||
if (path.startsWith("/dashboard")) return ["Home", "Analytics", "Overview"];
|
||||
if (path.startsWith("/jobs")) return ["Home", t("jobApplications")];
|
||||
if (path.startsWith("/reminders")) return ["Home", t("reminders")];
|
||||
if (path.startsWith("/kanban")) return ["Home", t("kanbanBoard")];
|
||||
if (path.startsWith("/companies")) return ["Home", t("companies")];
|
||||
if (path.startsWith("/trash")) return ["Home", t("trash")];
|
||||
if (path.startsWith("/settings")) return ["Home", t("settings")];
|
||||
if (path.startsWith("/profile")) return ["Home", "Account", "Profile"];
|
||||
if (path.startsWith("/admin/audit")) return ["Home", "Admin", "Audit"];
|
||||
if (path.startsWith("/admin/users")) return ["Home", "Admin", "Users"];
|
||||
if (path.startsWith("/admin/system")) return ["Home", "Admin", "System"];
|
||||
return ["Home"];
|
||||
if (path.startsWith("/dashboard")) return [t("home"), t("analytics"), t("overview")];
|
||||
if (path.startsWith("/jobs")) return [t("home"), t("jobApplications")];
|
||||
if (path.startsWith("/reminders")) return [t("home"), t("reminders")];
|
||||
if (path.startsWith("/kanban")) return [t("home"), t("kanbanBoard")];
|
||||
if (path.startsWith("/companies")) return [t("home"), t("companies")];
|
||||
if (path.startsWith("/trash")) return [t("home"), t("trash")];
|
||||
if (path.startsWith("/settings")) return [t("home"), t("settings")];
|
||||
if (path.startsWith("/profile")) return [t("home"), t("account"), t("profile")];
|
||||
if (path.startsWith("/admin/audit")) return [t("home"), t("admin"), t("auditLog")];
|
||||
if (path.startsWith("/admin/users")) return [t("home"), t("admin"), t("users")];
|
||||
if (path.startsWith("/admin/system")) return [t("home"), t("admin"), t("system")];
|
||||
return [t("home")];
|
||||
}
|
||||
|
||||
function titleFor(path: string, t: (k: any) => string): string {
|
||||
@@ -69,17 +81,17 @@ function titleFor(path: string, t: (k: any) => string): string {
|
||||
if (path.startsWith("/companies")) return t("companies");
|
||||
if (path.startsWith("/trash")) return t("trash");
|
||||
if (path.startsWith("/settings")) return t("settings");
|
||||
if (path.startsWith("/profile")) return "Profile";
|
||||
if (path.startsWith("/admin/audit")) return "Audit log";
|
||||
if (path.startsWith("/admin/users")) return "Users";
|
||||
if (path.startsWith("/admin/system")) return "System status";
|
||||
if (path.startsWith("/profile")) return t("profile");
|
||||
if (path.startsWith("/admin/audit")) return t("auditLog");
|
||||
if (path.startsWith("/admin/users")) return t("users");
|
||||
if (path.startsWith("/admin/system")) return t("systemStatus");
|
||||
return t("appTitle");
|
||||
}
|
||||
|
||||
function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMode, onThemeModeChange, accentColor, onAccentColorChange, onResetAccentColor }: { jobPageSize: 15 | 20 | 25; setJobPageSize: (n: 15 | 20 | 25) => void; jobColumns: JobTableColumns; setJobColumns: (c: JobTableColumns) => void; themeMode: ThemeModePref; onThemeModeChange: (v: ThemeModePref) => void; accentColor: string; onAccentColorChange: (v: string) => void; onResetAccentColor: () => void; }) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { language, setLanguage, t } = useI18n();
|
||||
const { t } = useI18n();
|
||||
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [quickOpen, setQuickOpen] = useState(false);
|
||||
@@ -126,31 +138,28 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
||||
const breadcrumbs = breadcrumbsFor(path, t);
|
||||
const setAndPersistPageSize = (n: 15 | 20 | 25) => { setJobPageSize(n); window.localStorage.setItem("jobPageSize", String(n)); };
|
||||
const setAndPersistColumns = (next: JobTableColumns) => { setJobColumns(next); window.localStorage.setItem("jobColumns", JSON.stringify(next)); };
|
||||
const fullName = [me?.firstName, me?.lastName].filter(Boolean).join(" ");
|
||||
|
||||
const nav: NavItem[] = [
|
||||
{ to: "/dashboard", label: t("dashboard"), icon: <DashboardIcon fontSize="small" />, section: "Manage" },
|
||||
{ to: "/jobs", label: t("jobApplications"), icon: <WorkOutlineIcon fontSize="small" />, section: "Manage" },
|
||||
{ to: "/reminders", label: t("reminders"), icon: <AlarmIcon fontSize="small" />, badgeCount: notifCount, section: "Manage" },
|
||||
{ to: "/kanban", label: t("kanbanBoard"), icon: <ViewKanbanIcon fontSize="small" />, section: "Manage" },
|
||||
{ to: "/companies", label: t("companies"), icon: <BusinessIcon fontSize="small" />, section: "Manage" },
|
||||
{ to: "/trash", label: t("trash"), icon: <DeleteOutlineIcon fontSize="small" />, section: "Manage" },
|
||||
{ to: "/dashboard", label: t("dashboard"), icon: <DashboardIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/jobs", label: t("jobApplications"), icon: <WorkOutlineIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/reminders", label: t("reminders"), icon: <AlarmIcon fontSize="small" />, badgeCount: notifCount, section: t("manage") },
|
||||
{ to: "/kanban", label: t("kanbanBoard"), icon: <ViewKanbanIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/companies", label: t("companies"), icon: <BusinessIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/trash", label: t("trash"), icon: <DeleteOutlineIcon fontSize="small" />, section: t("manage") },
|
||||
];
|
||||
|
||||
const navBottom: NavItem[] = [
|
||||
{ to: "/admin/audit", label: "Audit log", icon: <ShieldIcon fontSize="small" />, hidden: !isAdmin, section: "Admin" },
|
||||
{ to: "/admin/users", label: "Users", icon: <AccountCircleIcon fontSize="small" />, hidden: !isAdmin, section: "Admin" },
|
||||
{ to: "/admin/system", label: "System", icon: <MemoryIcon fontSize="small" />, hidden: !isAdmin, section: "Admin" },
|
||||
{ to: "/profile", label: "Profile", icon: <AccountCircleIcon fontSize="small" />, section: "Account" },
|
||||
{ to: "/settings", label: t("settings"), icon: <SettingsIcon fontSize="small" />, section: "Account" },
|
||||
{ to: "/admin/audit", label: t("auditLog"), icon: <ShieldIcon fontSize="small" />, hidden: !isAdmin, section: t("admin") },
|
||||
{ to: "/admin/users", label: t("users"), icon: <AccountCircleIcon fontSize="small" />, hidden: !isAdmin, section: t("admin") },
|
||||
{ to: "/admin/system", label: t("system"), icon: <MemoryIcon fontSize="small" />, hidden: !isAdmin, section: t("admin") },
|
||||
{ to: "/profile", label: t("profile"), icon: <AccountCircleIcon fontSize="small" />, section: t("account") },
|
||||
{ to: "/settings", label: t("settings"), icon: <SettingsIcon fontSize="small" />, section: t("account") },
|
||||
];
|
||||
|
||||
const rightActions = (
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button variant="outlined" startIcon={<SearchIcon />} onClick={() => setQuickOpen(true)}>Quick Search</Button>
|
||||
<ToggleButtonGroup size="small" exclusive value={language} onChange={(_, v) => v && setLanguage(v)}>
|
||||
<ToggleButton value="en">EN</ToggleButton>
|
||||
<ToggleButton value="no">NO</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Button variant="outlined" startIcon={<SearchIcon />} onClick={() => setQuickOpen(true)}>{t("quickSearch")}</Button>
|
||||
{isJobs ? <Button variant="contained" onClick={() => setAddOpen(true)}>{t("addJob")}</Button> : null}
|
||||
</Box>
|
||||
);
|
||||
@@ -166,7 +175,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
||||
drawerOpen={mobileDrawerOpen}
|
||||
onToggleDrawer={setMobileDrawerOpen}
|
||||
onNavigate={(to) => { setMobileDrawerOpen(false); navigate(to); }}
|
||||
user={{ email: me?.email, userName: me?.userName, roleLabel: isAdmin ? "Super Admin" : "User" }}
|
||||
user={{ email: me?.email, userName: me?.userName, displayName: me?.displayName || fullName || undefined, avatarImageDataUrl: me?.avatarImageDataUrl, roleLabel: isAdmin ? t("superAdmin") : t("user") }}
|
||||
notificationsCount={notifCount}
|
||||
onOpenNotifications={() => navigate("/reminders")}
|
||||
onOpenSettings={() => navigate("/settings")}
|
||||
@@ -187,7 +196,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
||||
<Route path="/admin/system" element={<AdminSystemPage />} />
|
||||
<Route path="/trash" element={<JobTable refreshToken={refreshToken} pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} mode="trash" />} />
|
||||
<Route path="/settings" element={<SettingsView pageSize={jobPageSize} onPageSizeChange={setAndPersistPageSize} columns={jobColumns} onColumnsChange={setAndPersistColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} accentColor={accentColor} onAccentColorChange={onAccentColorChange} onResetAccentColor={onResetAccentColor} />} />
|
||||
<Route path="*" element={<Navigate to="/jobs" replace />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</AppShell>
|
||||
|
||||
@@ -231,9 +240,9 @@ export default function App() {
|
||||
});
|
||||
|
||||
const router = useMemo(() => createBrowserRouter([
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
{ path: "/reset-password", element: <ResetPasswordPage /> },
|
||||
{ path: "/*", element: <Shell jobPageSize={jobPageSize} setJobPageSize={setJobPageSize} jobColumns={jobColumns} setJobColumns={setJobColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} accentColor={accentColor} onAccentColorChange={onAccentColorChange} onResetAccentColor={onResetAccentColor} /> },
|
||||
{ path: "/login", element: <LoginPage />, errorElement: <RouteErrorPage /> },
|
||||
{ path: "/reset-password", element: <ResetPasswordPage />, errorElement: <RouteErrorPage /> },
|
||||
{ path: "/*", element: <Shell jobPageSize={jobPageSize} setJobPageSize={setJobPageSize} jobColumns={jobColumns} setJobColumns={setJobColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} accentColor={accentColor} onAccentColorChange={onAccentColorChange} onResetAccentColor={onResetAccentColor} />, errorElement: <RouteErrorPage /> },
|
||||
], { future: { v7_relativeSplatPath: true } }), [jobColumns, jobPageSize, themeMode, accentColor]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user