Save workspace changes

This commit is contained in:
2026-04-18 17:02:56 +02:00
parent f02ea9a711
commit 87d60af5a9
4220 changed files with 1388603 additions and 1554 deletions

View File

@@ -168,6 +168,8 @@ export default function ProfileEdit() {
usernameCooldownDays = 30,
usernameCooldownRemainingDays = 0,
usernameCooldownActive = false,
emailLoginUpgradeRequired: initialEmailLoginUpgradeRequired = false,
forcedSection = null,
captcha: initialCaptcha = {},
flash = {},
} = props
@@ -178,7 +180,8 @@ export default function ProfileEdit() {
birthYear ? String(birthYear) : '',
)
const [activeSection, setActiveSection] = useState('profile')
const [emailLoginUpgradeRequired, setEmailLoginUpgradeRequired] = useState(!!initialEmailLoginUpgradeRequired)
const [activeSection, setActiveSection] = useState(forcedSection || 'profile')
const [profileForm, setProfileForm] = useState({
display_name: user?.name || '',
@@ -271,6 +274,14 @@ export default function ProfileEdit() {
const [deleteError, setDeleteError] = useState('')
const [deleting, setDeleting] = useState(false)
const settingsSections = useMemo(() => {
if (!emailLoginUpgradeRequired) {
return SETTINGS_SECTIONS
}
return SETTINGS_SECTIONS.filter((section) => section.key === 'account')
}, [emailLoginUpgradeRequired])
const initialRef = useRef({
profileForm,
accountForm,
@@ -316,12 +327,25 @@ export default function ProfileEdit() {
return () => window.removeEventListener('beforeunload', beforeUnload)
}, [hasUnsavedChanges])
useEffect(() => {
if (!emailLoginUpgradeRequired) {
return
}
setActiveSection('account')
}, [emailLoginUpgradeRequired])
useEffect(() => {
if (usernameCooldownActive) {
setUsernameAvailability({ status: 'idle', message: '' })
return
}
if (emailLoginUpgradeRequired) {
setUsernameAvailability({ status: 'idle', message: '' })
return
}
const candidate = String(accountForm.username || '').trim().toLowerCase()
const current = String(initialRef.current.accountForm.username || '').trim().toLowerCase()
@@ -370,7 +394,7 @@ export default function ProfileEdit() {
controller.abort()
window.clearTimeout(timeout)
}
}, [accountForm.username, usernameCooldownActive])
}, [accountForm.username, usernameCooldownActive, emailLoginUpgradeRequired])
const openEmailChangeModal = () => {
setShowEmailChangeModal(true)
@@ -503,6 +527,10 @@ export default function ProfileEdit() {
}
const switchSection = (nextSection) => {
if (emailLoginUpgradeRequired && nextSection !== 'account') {
return
}
if (activeSection === nextSection) return
if (dirtyMap[activeSection]) {
const shouldContinue = window.confirm('You have unsaved changes in this section. Leave without saving?')
@@ -593,6 +621,10 @@ export default function ProfileEdit() {
const saveAccountSection = async (event) => {
event.preventDefault()
if (emailLoginUpgradeRequired) {
return
}
if (usernameCooldownActive && accountForm.username !== initialRef.current.accountForm.username) {
updateSectionErrors('account', {
username: [`Username can be changed again in ${usernameCooldownRemainingDays} days.`],
@@ -701,6 +733,7 @@ export default function ProfileEdit() {
const nextEmail = payload.email || emailChangeForm.new_email
setAccountForm((prev) => ({ ...prev, email: nextEmail }))
initialRef.current.accountForm = { ...initialRef.current.accountForm, email: nextEmail }
setEmailLoginUpgradeRequired(false)
setShowEmailChangeModal(false)
setEmailChangeStep('request')
setEmailChangeForm({ new_email: '', code: '' })
@@ -910,7 +943,7 @@ export default function ProfileEdit() {
return (
<SettingsLayout
title="Settings"
sections={SETTINGS_SECTIONS}
sections={settingsSections}
activeSection={activeSection}
onSectionChange={switchSection}
dirtyMap={dirtyMap}
@@ -1103,10 +1136,10 @@ export default function ProfileEdit() {
{activeSection === 'account' ? (
<form className="space-y-4" onSubmit={saveAccountSection}>
<SectionCard
title="Account"
title={emailLoginUpgradeRequired ? 'Finish Email Upgrade' : 'Account'}
icon="fa-solid fa-id-badge"
description="Update your core account identity details."
actionSlot={
description={emailLoginUpgradeRequired ? 'Add and verify a real email address. Once completed, email becomes your only login identifier.' : 'Update your core account identity details.'}
actionSlot={!emailLoginUpgradeRequired ? (
<Button
type="submit"
variant="accent"
@@ -1116,11 +1149,25 @@ export default function ProfileEdit() {
>
Save Username
</Button>
}
) : null}
>
<ErrorMessage text={errorsBySection.account._general?.[0]} className="mb-4" />
<SuccessMessage text={sectionSaved} className="mb-4" />
{emailLoginUpgradeRequired ? (
<div className="mb-4 rounded-2xl border border-amber-400/30 bg-amber-500/10 p-4 text-sm text-amber-100">
<div className="flex items-start gap-3">
<i className="fa-solid fa-envelope-circle-check mt-0.5 shrink-0 text-amber-300" />
<div>
<p className="font-semibold text-amber-200">Email upgrade required</p>
<p className="mt-1 text-amber-100/90">
This account was migrated without a real email address. Add a valid email, enter the verification code, and future sign-ins will use email only.
</p>
</div>
</div>
</div>
) : null}
<div className="grid gap-4 md:grid-cols-2">
<TextInput
label="Username"
@@ -1129,23 +1176,26 @@ export default function ProfileEdit() {
setAccountForm((prev) => ({ ...prev, username: e.target.value }))
clearSectionStatus('account')
}}
disabled={usernameCooldownActive}
disabled={emailLoginUpgradeRequired || usernameCooldownActive}
error={errorsBySection.account.username?.[0]}
hint={usernameCooldownActive ? `Username can be changed again in ${usernameCooldownRemainingDays} days.` : 'Allowed: letters, numbers, underscores (3-20).'}
hint={emailLoginUpgradeRequired ? 'Username login is temporary. Complete the email upgrade to switch this account to email-only sign-in.' : usernameCooldownActive ? `Username can be changed again in ${usernameCooldownRemainingDays} days.` : 'Allowed: letters, numbers, underscores (3-20).'}
required
/>
<div className="rounded-xl border border-white/10 bg-black/20 p-4">
<p className="text-xs uppercase tracking-wide text-slate-400">Current Email</p>
<p className="text-xs uppercase tracking-wide text-slate-400">{emailLoginUpgradeRequired ? 'Primary Login Email' : 'Current Email'}</p>
<p className="mt-1 text-sm font-medium text-white">{accountForm.email || 'No email set'}</p>
{emailLoginUpgradeRequired ? (
<p className="mt-2 text-xs text-slate-400">The current value is temporary. Replace it with a real email address to finish the upgrade.</p>
) : null}
<div className="mt-3">
<Button type="button" variant="secondary" size="sm" onClick={openEmailChangeModal}>
Change Email
<Button type="button" variant={emailLoginUpgradeRequired ? 'accent' : 'secondary'} size="sm" onClick={openEmailChangeModal}>
{emailLoginUpgradeRequired ? 'Add Primary Email' : 'Change Email'}
</Button>
</div>
</div>
</div>
{usernameAvailability.status !== 'idle' ? (
{!emailLoginUpgradeRequired && usernameAvailability.status !== 'idle' ? (
<p
className={`mt-4 flex items-center gap-2 rounded-xl border px-3 py-2 text-xs ${
usernameAvailability.status === 'available'
@@ -1164,10 +1214,12 @@ export default function ProfileEdit() {
</p>
) : null}
<p className="mt-4 flex items-center gap-2 rounded-xl border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-xs text-slate-400">
<i className="fa-solid fa-clock shrink-0 text-slate-500" />
You can change your username once every {usernameCooldownDays} days.
</p>
{!emailLoginUpgradeRequired ? (
<p className="mt-4 flex items-center gap-2 rounded-xl border border-white/[0.06] bg-white/[0.02] px-3 py-2 text-xs text-slate-400">
<i className="fa-solid fa-clock shrink-0 text-slate-500" />
You can change your username once every {usernameCooldownDays} days.
</p>
) : null}
{renderCaptchaChallenge('account')}
</SectionCard>
@@ -1518,13 +1570,16 @@ export default function ProfileEdit() {
<Modal
open={showEmailChangeModal}
onClose={closeEmailChangeModal}
title="Change Email"
title={emailLoginUpgradeRequired ? 'Finish Email Upgrade' : 'Change Email'}
size="sm"
closeOnBackdrop={!emailLoginUpgradeRequired}
footer={
<div className="ml-auto flex items-center gap-2">
<Button type="button" variant="ghost" size="sm" onClick={closeEmailChangeModal} disabled={emailChangeLoading}>
Cancel
</Button>
{!emailLoginUpgradeRequired ? (
<Button type="button" variant="ghost" size="sm" onClick={closeEmailChangeModal} disabled={emailChangeLoading}>
Cancel
</Button>
) : null}
{emailChangeStep === 'request' ? (
<Button
type="button"
@@ -1550,6 +1605,12 @@ export default function ProfileEdit() {
}
>
<div className="space-y-4">
{emailLoginUpgradeRequired ? (
<div className="rounded-lg border border-sky-400/30 bg-sky-500/10 px-3 py-2 text-sm text-sky-200">
Add the email address you want to use for future sign-ins, then enter the verification code we send there.
</div>
) : null}
{emailChangeError ? (
<div className="rounded-lg border border-red-400/30 bg-red-500/10 px-3 py-2 text-sm text-red-300">
{emailChangeError}