feat(ui): separate career and connected accounts
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
# Jobbjakt Architecture Evolution
|
||||||
|
|
||||||
|
## Guiding constraints
|
||||||
|
|
||||||
|
- Preserve the existing single-master-profile model.
|
||||||
|
- Keep job applications and their workflow at the centre of the domain.
|
||||||
|
- Make AI providers replaceable and optional.
|
||||||
|
- Add SaaS foundations without prematurely building a billing platform or team product.
|
||||||
|
- Keep feature slices small while reducing the current controller concentration.
|
||||||
|
|
||||||
|
## Target boundaries
|
||||||
|
|
||||||
|
```text
|
||||||
|
Web UI
|
||||||
|
-> Application APIs
|
||||||
|
-> Application workflow domain
|
||||||
|
-> Career profile and document domain
|
||||||
|
-> Discovery provider adapters
|
||||||
|
-> Connected-account adapters
|
||||||
|
-> AI provider boundary
|
||||||
|
-> Entitlements and usage policy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Career profile and documents
|
||||||
|
|
||||||
|
`ApplicationUser.ProfileCvStructureJson` remains the canonical master profile during the transition. A later migration may normalize high-change sections (projects and portfolio items) into user-owned tables, but generated CVs and application-specific drafts must remain immutable copies with a profile-version reference.
|
||||||
|
|
||||||
|
### Discovery
|
||||||
|
|
||||||
|
Create an `IJobDiscoveryProvider` contract with normalized query and result types. Providers own authentication, source-specific filters, terms-aware attribution, and mapping. The UI and import flow must not know provider payload shapes. Cache public results briefly, do not persist an external job until the user imports it.
|
||||||
|
|
||||||
|
### AI
|
||||||
|
|
||||||
|
Extract AI prompts and model transport from controllers behind task-focused interfaces: CV extraction, CV tailoring, draft generation, interview preparation, and summarization. Each request records provider, model, input profile version, job context, and user review state. Local and online providers implement the same boundary.
|
||||||
|
|
||||||
|
### Entitlements
|
||||||
|
|
||||||
|
Introduce account-level plan, entitlement, and usage-ledger records before payment integration. Product code checks named capabilities (for example `ai.tailor_cv`, `cv.premium_theme`, `storage.attachments`) rather than payment-provider state. This supports trials, manual access, subscriptions, and future organisation plans.
|
||||||
|
|
||||||
|
## Incremental engineering work
|
||||||
|
|
||||||
|
1. Add targeted feature services beside the existing controllers for every touched feature; do not attempt a big-bang controller rewrite.
|
||||||
|
2. Replace local-only preferences that affect behaviour with user-owned persisted settings.
|
||||||
|
3. Replace the in-memory CV queue with a database-backed work item before horizontal hosting.
|
||||||
|
4. Use PostgreSQL for hosted multi-tenant deployment; retain SQLite for self-hosted/single-user deployments.
|
||||||
|
5. Add provider health, job processing, AI usage, and entitlement audit telemetry before paid hosting.
|
||||||
|
|
||||||
|
## Explicit non-goals for the first SaaS release
|
||||||
|
|
||||||
|
- Teams/organisations and shared candidate data.
|
||||||
|
- Auto-apply automation or scraping-proxy infrastructure.
|
||||||
|
- A browser-extension marketplace release.
|
||||||
|
- A billing-provider integration before commercial policy is decided.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Jobbjakt Product Evolution Plan
|
||||||
|
|
||||||
|
> Confirmed 2026-07-15. This document is the product decision record for the career-management evolution.
|
||||||
|
|
||||||
|
## Product position
|
||||||
|
|
||||||
|
Jobbjakt is a job-tracking and career-management product. The application workflow remains the core loop:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Discover opportunity -> Track application -> Prepare application -> Follow up -> Learn and progress
|
||||||
|
```
|
||||||
|
|
||||||
|
Career profile, CV generation, cover letters, portfolio, interview preparation, and discovery support this loop. They must not displace it or turn Jobbjakt into a CV-builder-first product.
|
||||||
|
|
||||||
|
## Confirmed decisions
|
||||||
|
|
||||||
|
| Decision | Direction |
|
||||||
|
| --- | --- |
|
||||||
|
| Primary discovery market | Nordics and Europe; global later |
|
||||||
|
| Discovery sources | Permitted provider APIs and existing URL capture; no scraping or browser extension in the first release |
|
||||||
|
| Career workspace | Dedicated top-level navigation item |
|
||||||
|
| Add-job flow | Wizard; create the application after job review, then keep preparation steps optional and resumable |
|
||||||
|
| Portfolio | Lightweight links and project items connected to the master career profile |
|
||||||
|
| AI | Retain local and online capability behind a provider boundary and explicit user controls |
|
||||||
|
| SaaS priority | Hosted subscriptions and feature entitlements before organisations/teams |
|
||||||
|
| Brand | Keep the Jobbjakt name |
|
||||||
|
|
||||||
|
## Target information architecture
|
||||||
|
|
||||||
|
| Area | Purpose | Initial destinations |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Dashboard | Daily direction | priorities, follow-ups, recent activity |
|
||||||
|
| Applications | Core tracker | list, board, companies, application workspace |
|
||||||
|
| Career Workspace | Reusable career source of truth | master profile, CV builder, portfolio, interview preparation |
|
||||||
|
| Discover Jobs | Find then act | provider search, saved searches later, import into tracker |
|
||||||
|
| Settings | Product preferences | general, follow-ups, notifications, connected accounts, data |
|
||||||
|
| Account | Identity and security | personal details, password, providers, sessions, 2FA |
|
||||||
|
|
||||||
|
## Delivery roadmap
|
||||||
|
|
||||||
|
### Phase 1 — Workflow clarity
|
||||||
|
|
||||||
|
1. Create the Career Workspace and move CV-facing functionality out of Account Profile.
|
||||||
|
2. Move Gmail, Outlook, and IMAP management to Settings > Connected Accounts.
|
||||||
|
3. Reorganise Settings: remove normal-user export/import controls, hide admin-only status controls, and improve follow-up explanations.
|
||||||
|
4. Simplify application workspace navigation into task-oriented sections.
|
||||||
|
|
||||||
|
### Phase 2 — Application preparation
|
||||||
|
|
||||||
|
1. Replace the dense Add Job modal with a six-step wizard.
|
||||||
|
2. URL capture and extraction lead the flow; manual details remain an easy fallback.
|
||||||
|
3. Save the application after review; CV, cover letter, portfolio, and documents are explicitly optional.
|
||||||
|
4. Link job-specific CV copies and draft material to the application without mutating the master profile.
|
||||||
|
|
||||||
|
### Phase 3 — Career workspace completion
|
||||||
|
|
||||||
|
1. Strengthen the structured master career profile: experience, education, skills, projects, certifications, languages, achievements, and portfolio links.
|
||||||
|
2. Turn the existing template system into a guided three-step CV builder: theme, preview/customise, save defaults.
|
||||||
|
3. Keep five professional templates and persisted visual preferences; add drag-and-drop ordering after the underlying configuration is durable.
|
||||||
|
|
||||||
|
### Phase 4 — Nordic/European discovery
|
||||||
|
|
||||||
|
1. Introduce a provider-adapter contract and a normalized result model.
|
||||||
|
2. Implement approved public/provider APIs beginning with Nordic sources; add European sources only when their terms permit product use.
|
||||||
|
3. Provide search, filters, source attribution, map/distance enrichment where data permits, and one-click import to the wizard.
|
||||||
|
4. Add fuzzy role matching locally through a role/skill synonym catalog before any external AI ranking.
|
||||||
|
|
||||||
|
### Phase 5 — Hosted SaaS foundations
|
||||||
|
|
||||||
|
1. Add account-level entitlement and usage records independent of a billing provider.
|
||||||
|
2. Gate premium themes, AI allowances, storage, and discovery quotas through policy checks.
|
||||||
|
3. Integrate a billing provider only after pricing, tax, trial, cancellation, and support policy decisions are recorded.
|
||||||
|
4. Move restart-sensitive CV processing to durable storage before multi-instance hosting.
|
||||||
|
|
||||||
|
## Success measures
|
||||||
|
|
||||||
|
- A new user can create a tracked application from a URL in under two minutes.
|
||||||
|
- A user can find their master profile, create a CV, and manage email connections without visiting Account Profile.
|
||||||
|
- Optional preparation never blocks capture of an opportunity.
|
||||||
|
- Every AI-generated output visibly states its source profile/job context and requires user review before it is saved or sent.
|
||||||
|
- Discovery always preserves source attribution and sends users to the original job advert to apply.
|
||||||
@@ -16,6 +16,7 @@ import ShieldIcon from "@mui/icons-material/Shield";
|
|||||||
import SearchIcon from "@mui/icons-material/Search";
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
import MailOutlineIcon from "@mui/icons-material/MailOutline";
|
import MailOutlineIcon from "@mui/icons-material/MailOutline";
|
||||||
import MemoryIcon from "@mui/icons-material/Memory";
|
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";
|
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 RemindersView = lazy(() => import("./components/RemindersView"));
|
||||||
const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog"));
|
const QuickCommandDialog = lazy(() => import("./components/QuickCommandDialog"));
|
||||||
const ProfilePage = lazy(() => import("./views/ProfilePage"));
|
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 AdminAuditPage = lazy(() => import("./views/AdminAuditPage"));
|
||||||
const AdminUsersPage = lazy(() => import("./views/AdminUsersPage"));
|
const AdminUsersPage = lazy(() => import("./views/AdminUsersPage"));
|
||||||
const AdminSystemPage = lazy(() => import("./views/AdminSystemPage"));
|
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("/trash")) return [t("home"), t("trash")];
|
||||||
if (path.startsWith("/settings")) return [t("home"), t("settings")];
|
if (path.startsWith("/settings")) return [t("home"), t("settings")];
|
||||||
if (path.startsWith("/profile")) return [t("home"), t("account"), t("profile")];
|
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/audit")) return [t("home"), t("admin"), t("auditLog")];
|
||||||
if (path.startsWith("/admin/users")) return [t("home"), t("admin"), t("users")];
|
if (path.startsWith("/admin/users")) return [t("home"), t("admin"), t("users")];
|
||||||
if (path.startsWith("/admin/system")) return [t("home"), t("admin"), t("system")];
|
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("/trash")) return t("trash");
|
||||||
if (path.startsWith("/settings")) return t("settings");
|
if (path.startsWith("/settings")) return t("settings");
|
||||||
if (path.startsWith("/profile")) return t("profile");
|
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/audit")) return t("auditLog");
|
||||||
if (path.startsWith("/admin/users")) return t("users");
|
if (path.startsWith("/admin/users")) return t("users");
|
||||||
if (path.startsWith("/admin/system")) return t("systemStatus");
|
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: "/companies", label: t("companies"), icon: <BusinessIcon fontSize="small" />, section: t("manage") },
|
||||||
{ to: "/correspondence", label: "Correspondence", icon: <MailOutlineIcon 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: "/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") },
|
{ 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" element={<CorrespondenceInboxPage />} />
|
||||||
<Route path="/correspondence/review" element={<GmailReviewPage />} />
|
<Route path="/correspondence/review" element={<GmailReviewPage />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
<Route path="/career" element={<CareerWorkspacePage />} />
|
||||||
<Route path="/admin/audit" element={<AdminAuditPage />} />
|
<Route path="/admin/audit" element={<AdminAuditPage />} />
|
||||||
<Route path="/admin/users" element={<AdminUsersPage />} />
|
<Route path="/admin/users" element={<AdminUsersPage />} />
|
||||||
<Route path="/admin/system" element={<AdminSystemPage />} />
|
<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="/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" 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 />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -198,6 +198,9 @@ export default function SettingsView({
|
|||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<QuickCaptureCard />
|
<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>
|
</Box>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
@@ -226,7 +229,6 @@ export default function SettingsView({
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
|
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
|
||||||
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
|
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
|
||||||
<Button variant="text" onClick={() => navigate("/admin/system")}>{t("settingsCheckSystemStatus")}</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
</TabPanel>
|
</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 AuthStatusCard from "../components/AuthStatusCard";
|
||||||
import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
|
import TwoFactorSettingsCard from "../components/TwoFactorSettingsCard";
|
||||||
import SessionsSettingsCard from "../components/SessionsSettingsCard";
|
import SessionsSettingsCard from "../components/SessionsSettingsCard";
|
||||||
import EmailProviderConnections from "../components/EmailProviderConnections";
|
|
||||||
import CropImageDialog from "../components/CropImageDialog";
|
import CropImageDialog from "../components/CropImageDialog";
|
||||||
import { useToast } from "../toast";
|
import { useToast } from "../toast";
|
||||||
import { useI18n } from "../i18n/I18nProvider";
|
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 { toast } = useToast();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const cvInputRef = useRef<HTMLInputElement | null>(null);
|
const cvInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
@@ -553,7 +552,7 @@ export default function ProfilePage() {
|
|||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 900 }}>
|
<Typography variant="h5" sx={{ fontWeight: 900 }}>
|
||||||
{t("profileTitle")}
|
{careerOnly ? "Master career profile" : t("profileTitle")}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography sx={{ color: "text.secondary" }}>{me?.userName || me?.displayName || fullName || me?.email || "-"}</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>
|
<Typography variant="body2" sx={{ color: "text.secondary" }}>{headline || t("profileHeadlinePlaceholder")}</Typography>
|
||||||
@@ -566,37 +565,38 @@ export default function ProfilePage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<AuthStatusCard />
|
{!careerOnly ? <>
|
||||||
<GoogleAuthCard />
|
<AuthStatusCard />
|
||||||
<MicrosoftAuthCard />
|
<GoogleAuthCard />
|
||||||
<Box sx={{ mt: 2 }}>
|
<MicrosoftAuthCard />
|
||||||
<EmailProviderConnections />
|
</> : null}
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ mt: 3, display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
<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>
|
<Typography variant="h6">{t("profileAccountSection")}</Typography>
|
||||||
{!isLocal ? (
|
{!isLocal ? (
|
||||||
<Alert severity="info" sx={{ mt: 1 }}>
|
<Alert severity="info" sx={{ mt: 1 }}>
|
||||||
{t("profileReadOnlyInfo")}
|
{t("profileReadOnlyInfo")}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box> : null}
|
||||||
|
|
||||||
<TextField label={t("profileDisplayName")} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={!isLocal} fullWidth />
|
{!careerOnly ? <>
|
||||||
<TextField label={t("profileUsername")} value={userName} onChange={(e) => setUserName(e.target.value)} disabled={!isLocal} fullWidth />
|
<TextField label={t("profileDisplayName")} value={displayName} onChange={(e) => setDisplayName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||||
<TextField label={t("profileFirstName")} value={firstName} onChange={(e) => setFirstName(e.target.value)} disabled={!isLocal} fullWidth />
|
<TextField label={t("profileUsername")} value={userName} onChange={(e) => setUserName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||||
<TextField label={t("profileLastName")} value={lastName} onChange={(e) => setLastName(e.target.value)} disabled={!isLocal} fullWidth />
|
<TextField label={t("profileFirstName")} value={firstName} onChange={(e) => setFirstName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||||
<TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!isLocal} fullWidth />
|
<TextField label={t("profileLastName")} value={lastName} onChange={(e) => setLastName(e.target.value)} disabled={!isLocal} fullWidth />
|
||||||
<TextField
|
<TextField label={t("profileEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!isLocal} fullWidth />
|
||||||
|
<TextField
|
||||||
label={t("profileHeadline")}
|
label={t("profileHeadline")}
|
||||||
value={headline}
|
value={headline}
|
||||||
onChange={(e) => setHeadline(e.target.value)}
|
onChange={(e) => setHeadline(e.target.value)}
|
||||||
helperText={t("profileHeadlineHelp")}
|
helperText={t("profileHeadlineHelp")}
|
||||||
fullWidth
|
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 sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h6">{t("profileMasterCv")}</Typography>
|
<Typography variant="h6">{t("profileMasterCv")}</Typography>
|
||||||
@@ -1318,16 +1318,17 @@ export default function ProfilePage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
|
{!careerOnly ? <Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
|
||||||
<Divider sx={{ mb: 2 }} />
|
<Divider sx={{ mb: 2 }} />
|
||||||
<Typography variant="h6">{t("profileChangePassword")}</Typography>
|
<Typography variant="h6">{t("profileChangePassword")}</Typography>
|
||||||
{!isLocal ? <Typography sx={{ color: "text.secondary" }}>{t("profilePasswordLocalOnly")}</Typography> : null}
|
{!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 />
|
{!careerOnly ? <>
|
||||||
<TextField label={t("profileNewPassword")} type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} disabled={!isLocal} fullWidth />
|
<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
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
disabled={!isLocal || loading}
|
disabled={!isLocal || loading}
|
||||||
@@ -1348,11 +1349,12 @@ export default function ProfilePage() {
|
|||||||
>
|
>
|
||||||
{t("profileUpdatePassword")}
|
{t("profileUpdatePassword")}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
</> : null}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{isLocal ? <TwoFactorSettingsCard /> : null}
|
{!careerOnly && isLocal ? <TwoFactorSettingsCard /> : null}
|
||||||
{isLocal ? <SessionsSettingsCard /> : null}
|
{!careerOnly && isLocal ? <SessionsSettingsCard /> : null}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user