feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
@@ -125,6 +125,11 @@ type MeResponse = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
type PendingEmailChange = {
|
||||
pendingEmail?: string | null;
|
||||
requestedAtUtc?: string | null;
|
||||
};
|
||||
|
||||
const CV_UPLOAD_ACCEPT = ".pdf,.docx,.txt,.md,image/png,image/jpeg,image/webp,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown";
|
||||
const AVATAR_UPLOAD_ACCEPT = "image/png,image/jpeg,image/webp";
|
||||
const REWRITE_TEMPLATES: RewriteTemplateOption[] = [
|
||||
@@ -165,7 +170,7 @@ const REWRITE_TEMPLATES: RewriteTemplateOption[] = [
|
||||
accent: "#5b21b6",
|
||||
blurb: "More personality and stronger section contrast without losing clarity.",
|
||||
sampleHeading: "Experience Highlights",
|
||||
sampleMeta: "Premium spacing · stronger visual voice",
|
||||
sampleMeta: "Refined spacing · stronger visual voice",
|
||||
sampleBullets: ["Useful when the CV should feel more distinctive.", "Still keeps wording grounded and factual."]
|
||||
},
|
||||
{
|
||||
@@ -173,9 +178,9 @@ const REWRITE_TEMPLATES: RewriteTemplateOption[] = [
|
||||
title: "Monarch",
|
||||
eyebrow: "Executive",
|
||||
accent: "#7c2d12",
|
||||
blurb: "High-contrast premium presentation for leadership-heavy applications.",
|
||||
blurb: "High-contrast presentation for leadership-heavy applications.",
|
||||
sampleHeading: "Executive Profile",
|
||||
sampleMeta: "Leadership clarity · premium hierarchy",
|
||||
sampleMeta: "Leadership clarity · refined hierarchy",
|
||||
sampleBullets: ["Adds more top-level summary emphasis.", "Well suited to senior strategic roles."]
|
||||
},
|
||||
{
|
||||
@@ -243,6 +248,8 @@ export default function ProfilePage() {
|
||||
const [cropOpen, setCropOpen] = useState(false);
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [pendingEmail, setPendingEmail] = useState<string | null>(null);
|
||||
const [emailChangePassword, setEmailChangePassword] = useState("");
|
||||
const [userName, setUserName] = useState("");
|
||||
const [firstName, setFirstName] = useState("");
|
||||
const [lastName, setLastName] = useState("");
|
||||
@@ -267,6 +274,12 @@ export default function ProfilePage() {
|
||||
setDisplayName(r.data?.displayName ?? "");
|
||||
setProfileCvText(r.data?.profileCvText ?? "");
|
||||
setHeadline(window.localStorage.getItem("profileHeadline") ?? "");
|
||||
if (r.data?.provider === "local") {
|
||||
const pending = await api.get<PendingEmailChange>("/auth/email-change");
|
||||
setPendingEmail(pending.data?.pendingEmail ?? null);
|
||||
} else {
|
||||
setPendingEmail(null);
|
||||
}
|
||||
setLoadError(null);
|
||||
} catch (error: any) {
|
||||
setMe(null);
|
||||
@@ -412,7 +425,7 @@ export default function ProfilePage() {
|
||||
<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("profileNewEmail")} value={email} onChange={(e) => setEmail(e.target.value)} disabled={!isLocal} helperText={t("profileCurrentEmail", { email: me?.email || "-" })} fullWidth />
|
||||
<TextField
|
||||
label={t("profileHeadline")}
|
||||
value={headline}
|
||||
@@ -422,6 +435,49 @@ export default function ProfilePage() {
|
||||
/>
|
||||
</> : null}
|
||||
|
||||
{!careerOnly && isLocal ? <Box sx={{ gridColumn: "1 / -1", display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr auto auto" }, gap: 2, alignItems: "center" }}>
|
||||
{pendingEmail ? <Alert severity="info" sx={{ gridColumn: "1 / -1" }}>{t("profilePendingEmail", { email: pendingEmail })}</Alert> : null}
|
||||
<TextField label={t("profileEmailChangePassword")} type="password" value={emailChangePassword} onChange={(e) => setEmailChangePassword(e.target.value)} autoComplete="current-password" fullWidth />
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={loading || !emailChangePassword || !email.trim() || email.trim().toLowerCase() === (me?.email || "").toLowerCase()}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await api.post<PendingEmailChange>("/auth/email-change/request", { email, currentPassword: emailChangePassword });
|
||||
setPendingEmail(result.data?.pendingEmail ?? email.trim());
|
||||
setEmailChangePassword("");
|
||||
toast(t("profileEmailChangeSent"), "success");
|
||||
} catch (e: any) {
|
||||
toast(getApiErrorMessage(e, t("profileEmailChangeFailed")), "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("profileRequestEmailChange")}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={loading || !pendingEmail || !emailChangePassword}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.post("/auth/email-change/cancel", { currentPassword: emailChangePassword });
|
||||
setPendingEmail(null);
|
||||
setEmail(me?.email ?? "");
|
||||
setEmailChangePassword("");
|
||||
toast(t("profileEmailChangeCancelled"), "success");
|
||||
} catch (e: any) {
|
||||
toast(getApiErrorMessage(e, t("profileEmailChangeFailed")), "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
</Box> : null}
|
||||
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end", gap: 2, flexWrap: "wrap", alignItems: "center" }}>
|
||||
<Button
|
||||
@@ -432,7 +488,7 @@ export default function ProfilePage() {
|
||||
try {
|
||||
// /profile saves identity only. The backend does partial updates, so omitting the
|
||||
// master-profile fields leaves them untouched (they are owned by /career).
|
||||
await api.put("/auth/profile", { email, userName, firstName, lastName, displayName });
|
||||
await api.put("/auth/profile", { userName, firstName, lastName, displayName });
|
||||
window.localStorage.setItem("profileHeadline", headline.trim());
|
||||
await loadProfile();
|
||||
toast(t("profileUpdated"), "success");
|
||||
|
||||
Reference in New Issue
Block a user