feat(ui): separate career and connected accounts
This commit is contained in:
@@ -16,6 +16,7 @@ import ShieldIcon from "@mui/icons-material/Shield";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
import MailOutlineIcon from "@mui/icons-material/MailOutline";
|
||||
import MemoryIcon from "@mui/icons-material/Memory";
|
||||
import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
|
||||
|
||||
import { Navigate, Route, Routes, useLocation, useNavigate, createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
@@ -47,6 +48,8 @@ const SettingsView = lazy(() => import("./components/SettingsView"));
|
||||
const RemindersView = lazy(() => import("./components/RemindersView"));
|
||||
const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog"));
|
||||
const ProfilePage = lazy(() => import("./views/ProfilePage"));
|
||||
const CareerWorkspacePage = lazy(() => import("./views/CareerWorkspacePage"));
|
||||
const ConnectedAccountsPage = lazy(() => import("./views/ConnectedAccountsPage"));
|
||||
const AdminAuditPage = lazy(() => import("./views/AdminAuditPage"));
|
||||
const AdminUsersPage = lazy(() => import("./views/AdminUsersPage"));
|
||||
const AdminSystemPage = lazy(() => import("./views/AdminSystemPage"));
|
||||
@@ -78,6 +81,8 @@ function breadcrumbsFor(path: string, t: (k: any) => string): string[] {
|
||||
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("/career")) return [t("home"), "Career Workspace"];
|
||||
if (path.startsWith("/settings/connected-accounts")) return [t("home"), t("settings"), "Connected accounts"];
|
||||
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")];
|
||||
@@ -95,6 +100,8 @@ function titleFor(path: string, t: (k: any) => string): string {
|
||||
if (path.startsWith("/trash")) return t("trash");
|
||||
if (path.startsWith("/settings")) return t("settings");
|
||||
if (path.startsWith("/profile")) return t("profile");
|
||||
if (path.startsWith("/career")) return "Career Workspace";
|
||||
if (path.startsWith("/settings/connected-accounts")) return "Connected accounts";
|
||||
if (path.startsWith("/admin/audit")) return t("auditLog");
|
||||
if (path.startsWith("/admin/users")) return t("users");
|
||||
if (path.startsWith("/admin/system")) return t("systemStatus");
|
||||
@@ -234,6 +241,7 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
||||
{ to: "/companies", label: t("companies"), icon: <BusinessIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/correspondence", label: "Correspondence", icon: <MailOutlineIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/correspondence/review", label: "Gmail review", icon: <MailOutlineIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/career", label: "Career Workspace", icon: <DescriptionOutlinedIcon fontSize="small" />, section: t("manage") },
|
||||
{ to: "/trash", label: t("trash"), icon: <DeleteOutlineIcon fontSize="small" />, section: t("manage") },
|
||||
];
|
||||
|
||||
@@ -314,11 +322,13 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
||||
<Route path="/correspondence" element={<CorrespondenceInboxPage />} />
|
||||
<Route path="/correspondence/review" element={<GmailReviewPage />} />
|
||||
<Route path="/profile" element={<ProfilePage />} />
|
||||
<Route path="/career" element={<CareerWorkspacePage />} />
|
||||
<Route path="/admin/audit" element={<AdminAuditPage />} />
|
||||
<Route path="/admin/users" element={<AdminUsersPage />} />
|
||||
<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} />} />
|
||||
<Route path="/settings/connected-accounts" element={<ConnectedAccountsPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
@@ -198,6 +198,9 @@ export default function SettingsView({
|
||||
</SectionCard>
|
||||
|
||||
<QuickCaptureCard />
|
||||
<SectionCard title="Connected accounts" subtitle="Manage inbox connections separately from your account and security settings.">
|
||||
<Button variant="outlined" onClick={() => navigate("/settings/connected-accounts")}>Manage connected accounts</Button>
|
||||
</SectionCard>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
@@ -226,7 +229,6 @@ export default function SettingsView({
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
|
||||
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
|
||||
<Button variant="text" onClick={() => navigate("/admin/system")}>{t("settingsCheckSystemStatus")}</Button>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
</TabPanel>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
|
||||
import { Alert, Box, Paper, Typography } from "@mui/material";
|
||||
|
||||
import ProfilePage from "./ProfilePage";
|
||||
|
||||
export default function CareerWorkspacePage() {
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: 2 }}>
|
||||
<Paper sx={{ p: 2.5, borderRadius: 4, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 900, mb: 0.5 }}>Career Workspace</Typography>
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
Maintain the master career profile that powers your CVs, tailored application material, and future portfolio outputs.
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Alert severity="info" sx={{ borderRadius: 3 }}>
|
||||
Your master profile is the source of truth. Job-specific CV drafts remain separate and never overwrite it.
|
||||
</Alert>
|
||||
<ProfilePage careerOnly />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
import { Box, Button, Paper, Typography } from "@mui/material";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import EmailProviderConnections from "../components/EmailProviderConnections";
|
||||
|
||||
export default function ConnectedAccountsPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "grid", gap: 2 }}>
|
||||
<Paper sx={{ p: 2.5, borderRadius: 4, boxShadow: "0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 900, mb: 0.5 }}>Connected accounts</Typography>
|
||||
<Typography sx={{ color: "text.secondary", maxWidth: 720 }}>
|
||||
Connect the inboxes Jobbjakt may use to identify recruiter correspondence. Connecting an inbox never sends a message on your behalf.
|
||||
</Typography>
|
||||
<Button sx={{ mt: 1 }} onClick={() => navigate("/settings")}>Back to settings</Button>
|
||||
</Paper>
|
||||
<EmailProviderConnections />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import MicrosoftAuthCard from "../components/MicrosoftAuthCard";
|
||||
import AuthStatusCard from "../components/AuthStatusCard";
|
||||
import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
|
||||
import SessionsSettingsCard from "../components/SessionsSettingsCard";
|
||||
import EmailProviderConnections from "../components/EmailProviderConnections";
|
||||
import CropImageDialog from "../components/CropImageDialog";
|
||||
import { useToast } from "../toast";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
@@ -227,7 +226,7 @@ function FieldReviewNote({ metadata }: { metadata?: StructuredCvFieldMetadata })
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
export default function ProfilePage({ careerOnly = false }: { careerOnly?: boolean }) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const cvInputRef = useRef<HTMLInputElement | null>(null);
|
||||
@@ -553,7 +552,7 @@ export default function ProfilePage() {
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 900 }}>
|
||||
{t("profileTitle")}
|
||||
{careerOnly ? "Master career profile" : t("profileTitle")}
|
||||
</Typography>
|
||||
<Typography sx={{ color: "text.secondary" }}>{me?.userName || me?.displayName || fullName || me?.email || "-"}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>{headline || t("profileHeadlinePlaceholder")}</Typography>
|
||||
@@ -566,37 +565,38 @@ export default function ProfilePage() {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<AuthStatusCard />
|
||||
<GoogleAuthCard />
|
||||
<MicrosoftAuthCard />
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<EmailProviderConnections />
|
||||
</Box>
|
||||
{!careerOnly ? <>
|
||||
<AuthStatusCard />
|
||||
<GoogleAuthCard />
|
||||
<MicrosoftAuthCard />
|
||||
</> : null}
|
||||
|
||||
<Box sx={{ mt: 3, display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
{!careerOnly ? <Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="h6">{t("profileAccountSection")}</Typography>
|
||||
{!isLocal ? (
|
||||
<Alert severity="info" sx={{ mt: 1 }}>
|
||||
{t("profileReadOnlyInfo")}
|
||||
</Alert>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box> : null}
|
||||
|
||||
<TextField label={t("profileDisplayName")} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileUsername")} value={userName} onChange={(e) => setUserName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileFirstName")} value={firstName} onChange={(e) => setFirstName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileLastName")} value={lastName} onChange={(e) => setLastName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField
|
||||
{!careerOnly ? <>
|
||||
<TextField label={t("profileDisplayName")} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileUsername")} value={userName} onChange={(e) => setUserName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileFirstName")} value={firstName} onChange={(e) => setFirstName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileLastName")} value={lastName} onChange={(e) => setLastName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField
|
||||
label={t("profileHeadline")}
|
||||
value={headline}
|
||||
onChange={(e) => setHeadline(e.target.value)}
|
||||
helperText={t("profileHeadlineHelp")}
|
||||
fullWidth
|
||||
/>
|
||||
/>
|
||||
</> : null}
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default" }}>
|
||||
<Box sx={{ gridColumn: "1 / -1", p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.default", display: careerOnly ? "block" : "none" }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
|
||||
<Box>
|
||||
<Typography variant="h6">{t("profileMasterCv")}</Typography>
|
||||
@@ -1318,16 +1318,17 @@ export default function ProfilePage() {
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
|
||||
{!careerOnly ? <Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
<Typography variant="h6">{t("profileChangePassword")}</Typography>
|
||||
{!isLocal ? <Typography sx={{ color: "text.secondary" }}>{t("profilePasswordLocalOnly")}</Typography> : null}
|
||||
</Box>
|
||||
</Box> : null}
|
||||
|
||||
<TextField label={t("profileCurrentPassword")} type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
{!careerOnly ? <>
|
||||
<TextField label={t("profileCurrentPassword")} type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} disabled={!isLocal} fullWidth />
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={!isLocal || loading}
|
||||
@@ -1348,11 +1349,12 @@ export default function ProfilePage() {
|
||||
>
|
||||
{t("profileUpdatePassword")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</> : null}
|
||||
</Box>
|
||||
|
||||
{isLocal ? <TwoFactorSettingsCard /> : null}
|
||||
{isLocal ? <SessionsSettingsCard /> : null}
|
||||
{!careerOnly && isLocal ? <TwoFactorSettingsCard /> : null}
|
||||
{!careerOnly && isLocal ? <SessionsSettingsCard /> : null}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user