import React, { useEffect, useMemo, useRef, useState } from 'react'
import { usePage } from '@inertiajs/react'
import SettingsLayout from '../../Layouts/SettingsLayout'
import TextInput from '../../components/ui/TextInput'
import Textarea from '../../components/ui/Textarea'
import Button from '../../components/ui/Button'
import Toggle from '../../components/ui/Toggle'
import Select from '../../components/ui/Select'
import NovaSelect from '../../components/ui/NovaSelect'
import Modal from '../../components/ui/Modal'
import { RadioGroup } from '../../components/ui/Radio'
import { buildBotFingerprint } from '../../lib/security/botFingerprint'
import TurnstileField from '../../components/security/TurnstileField'
const SETTINGS_SECTIONS = [
{ key: 'profile', label: 'Profile', icon: 'fa-solid fa-user-astronaut', description: 'Public identity and avatar.' },
{ key: 'account', label: 'Account', icon: 'fa-solid fa-id-badge', description: 'Username and email address.' },
{ key: 'personal', label: 'Personal', icon: 'fa-solid fa-address-card', description: 'Optional personal information.' },
{ key: 'notifications', label: 'Notifications', icon: 'fa-solid fa-bell', description: 'Manage notification preferences.' },
{ key: 'security', label: 'Security', icon: 'fa-solid fa-shield-halved', description: 'Password and account security.' },
{ key: 'danger', label: 'Danger Zone', icon: 'fa-solid fa-triangle-exclamation', description: 'Destructive account actions.' },
]
const MONTHS = [
{ value: '1', label: 'January' },
{ value: '2', label: 'February' },
{ value: '3', label: 'March' },
{ value: '4', label: 'April' },
{ value: '5', label: 'May' },
{ value: '6', label: 'June' },
{ value: '7', label: 'July' },
{ value: '8', label: 'August' },
{ value: '9', label: 'September' },
{ value: '10', label: 'October' },
{ value: '11', label: 'November' },
{ value: '12', label: 'December' },
]
const GENDER_OPTIONS = [
{ value: 'm', label: 'Male' },
{ value: 'f', label: 'Female' },
{ value: 'x', label: 'Prefer not to say' },
]
const AVATAR_POSITION_OPTIONS = [
{ value: 'top-left', label: 'Top Left' },
{ value: 'top', label: 'Top' },
{ value: 'top-right', label: 'Top Right' },
{ value: 'left', label: 'Left' },
{ value: 'center', label: 'Center' },
{ value: 'right', label: 'Right' },
{ value: 'bottom-left', label: 'Bottom Left' },
{ value: 'bottom', label: 'Bottom' },
{ value: 'bottom-right', label: 'Bottom Right' },
]
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
}
async function botHeaders(extra = {}, captcha = {}) {
const fingerprint = await buildBotFingerprint()
return {
...extra,
'X-Bot-Fingerprint': fingerprint,
...(captcha?.token ? { 'X-Captcha-Token': captcha.token } : {}),
}
}
function toIsoDate(day, month, year) {
if (!day || !month || !year) return ''
return `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`
}
function fromIsoDate(iso) {
if (!iso || typeof iso !== 'string' || !iso.includes('-')) {
return { day: '', month: '', year: '' }
}
const [year, month, day] = iso.split('-')
return {
day: String(parseInt(day || '', 10) || ''),
month: String(parseInt(month || '', 10) || ''),
year: String(year || ''),
}
}
function equalsObject(a, b) {
return JSON.stringify(a) === JSON.stringify(b)
}
function positionToObjectPosition(position) {
const map = {
'top-left': '0% 0%',
top: '50% 0%',
'top-right': '100% 0%',
left: '0% 50%',
center: '50% 50%',
right: '100% 50%',
'bottom-left': '0% 100%',
bottom: '50% 100%',
'bottom-right': '100% 100%',
}
return map[position] || '50% 50%'
}
function SectionCard({ title, description, children, actionSlot }) {
return (
{title}
{description ?
{description}
: null}
{actionSlot ? {actionSlot}
: null}
{children}
)
}
export default function ProfileEdit() {
const { props } = usePage()
const {
user,
avatarUrl: initialAvatarUrl,
birthDay,
birthMonth,
birthYear,
countries = [],
usernameCooldownDays = 30,
usernameCooldownRemainingDays = 0,
usernameCooldownActive = false,
captcha: initialCaptcha = {},
flash = {},
} = props
const fallbackDate = toIsoDate(
birthDay ? parseInt(String(birthDay), 10) : '',
birthMonth ? parseInt(String(birthMonth), 10) : '',
birthYear ? String(birthYear) : '',
)
const [activeSection, setActiveSection] = useState('profile')
const [profileForm, setProfileForm] = useState({
display_name: user?.name || '',
website: user?.homepage || '',
bio: user?.about_me || '',
signature: user?.signature || '',
description: user?.description || '',
})
const [accountForm, setAccountForm] = useState({
username: user?.username || '',
email: user?.email || '',
})
const [usernameAvailability, setUsernameAvailability] = useState({
status: 'idle',
message: '',
})
const [showEmailChangeModal, setShowEmailChangeModal] = useState(false)
const [emailChangeStep, setEmailChangeStep] = useState('request')
const [emailChangeLoading, setEmailChangeLoading] = useState(false)
const [emailChangeForm, setEmailChangeForm] = useState({
new_email: '',
code: '',
})
const [emailChangeError, setEmailChangeError] = useState('')
const [emailChangeInfo, setEmailChangeInfo] = useState('')
const [personalForm, setPersonalForm] = useState(() => {
const fromUser = fromIsoDate(user?.birthday || '')
const fromProps = {
day: birthDay ? String(parseInt(String(birthDay), 10) || '') : '',
month: birthMonth ? String(parseInt(String(birthMonth), 10) || '') : '',
year: birthYear ? String(birthYear) : '',
}
return {
day: fromUser.day || fromProps.day || '',
month: fromUser.month || fromProps.month || '',
year: fromUser.year || fromProps.year || '',
gender: String(user?.gender || '').toLowerCase() || '',
country_id: user?.country_id ? String(user.country_id) : '',
}
})
const [notificationForm, setNotificationForm] = useState({
email_notifications: !!user?.email_notifications,
upload_notifications: !!user?.upload_notifications,
follower_notifications: !!user?.follower_notifications,
comment_notifications: !!user?.comment_notifications,
newsletter: !!user?.newsletter,
})
const [securityForm, setSecurityForm] = useState({
current_password: '',
new_password: '',
new_password_confirmation: '',
})
const [savingSection, setSavingSection] = useState('')
const [savedMessage, setSavedMessage] = useState({ section: '', text: '' })
const [errorsBySection, setErrorsBySection] = useState({
profile: {},
account: {},
personal: {},
notifications: {},
security: {},
})
const [captchaState, setCaptchaState] = useState({
required: !!flash?.botCaptchaRequired,
section: '',
token: '',
message: '',
nonce: 0,
provider: initialCaptcha?.provider || '',
siteKey: initialCaptcha?.siteKey || '',
inputName: initialCaptcha?.inputName || 'cf-turnstile-response',
scriptUrl: initialCaptcha?.scriptUrl || '',
})
const [avatarUrl, setAvatarUrl] = useState(initialAvatarUrl || '')
const [avatarFile, setAvatarFile] = useState(null)
const [avatarPosition, setAvatarPosition] = useState('center')
const [dragActive, setDragActive] = useState(false)
const [removeAvatar, setRemoveAvatar] = useState(false)
const avatarInputRef = useRef(null)
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [deletePassword, setDeletePassword] = useState('')
const [deleteError, setDeleteError] = useState('')
const [deleting, setDeleting] = useState(false)
const initialRef = useRef({
profileForm,
accountForm,
personalForm,
notificationForm,
avatarUrl: initialAvatarUrl || '',
})
const dirtyMap = useMemo(() => {
return {
profile:
!equalsObject(profileForm, initialRef.current.profileForm) ||
!!avatarFile ||
(avatarFile && avatarPosition !== 'center') ||
removeAvatar ||
avatarUrl !== initialRef.current.avatarUrl,
account: !equalsObject(accountForm, initialRef.current.accountForm),
personal: !equalsObject(personalForm, initialRef.current.personalForm),
notifications: !equalsObject(notificationForm, initialRef.current.notificationForm),
security:
!!securityForm.current_password ||
!!securityForm.new_password ||
!!securityForm.new_password_confirmation,
danger: false,
}
}, [profileForm, accountForm, personalForm, notificationForm, securityForm, avatarFile, avatarPosition, removeAvatar, avatarUrl])
const hasUnsavedChanges = useMemo(
() => Object.entries(dirtyMap).some(([key, dirty]) => key !== 'danger' && dirty),
[dirtyMap],
)
useEffect(() => {
const beforeUnload = (event) => {
if (!hasUnsavedChanges) return
event.preventDefault()
event.returnValue = ''
}
window.addEventListener('beforeunload', beforeUnload)
return () => window.removeEventListener('beforeunload', beforeUnload)
}, [hasUnsavedChanges])
useEffect(() => {
if (usernameCooldownActive) {
setUsernameAvailability({ status: 'idle', message: '' })
return
}
const candidate = String(accountForm.username || '').trim().toLowerCase()
const current = String(initialRef.current.accountForm.username || '').trim().toLowerCase()
if (!candidate || candidate === current) {
setUsernameAvailability({ status: 'idle', message: '' })
return
}
if (!USERNAME_REGEX.test(candidate)) {
setUsernameAvailability({ status: 'invalid', message: 'Use 3-20 letters, numbers, or underscores.' })
return
}
setUsernameAvailability({ status: 'checking', message: 'Checking availability...' })
const controller = new AbortController()
const timeout = window.setTimeout(async () => {
try {
const response = await fetch(`/api/username/check?username=${encodeURIComponent(candidate)}`, {
credentials: 'same-origin',
headers: { Accept: 'application/json' },
signal: controller.signal,
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
const msg = payload?.errors?.username?.[0] || 'Username is not available.'
setUsernameAvailability({ status: 'invalid', message: msg })
return
}
if (payload.available) {
setUsernameAvailability({ status: 'available', message: 'Username is available.' })
return
}
setUsernameAvailability({ status: 'taken', message: 'Username is already taken.' })
} catch (error) {
if (error?.name !== 'AbortError') {
setUsernameAvailability({ status: 'invalid', message: 'Unable to check username right now.' })
}
}
}, 300)
return () => {
controller.abort()
window.clearTimeout(timeout)
}
}, [accountForm.username, usernameCooldownActive])
const openEmailChangeModal = () => {
setShowEmailChangeModal(true)
setEmailChangeStep('request')
setEmailChangeError('')
setEmailChangeInfo('')
setEmailChangeForm({ new_email: '', code: '' })
}
const closeEmailChangeModal = () => {
if (emailChangeLoading) return
setShowEmailChangeModal(false)
setEmailChangeStep('request')
setEmailChangeError('')
setEmailChangeInfo('')
setEmailChangeForm({ new_email: '', code: '' })
}
const countryOptions = (countries || []).map((c) => ({
value: String(c.id || ''),
label: c.name || '',
iso2: c.iso2 || '',
flagEmoji: c.flag_emoji || '',
flagPath: c.flag_path || '',
group: c.is_featured ? 'Featured' : 'All countries',
}))
const yearOptions = useMemo(() => {
const current = new Date().getFullYear()
return Array.from({ length: 100 }, (_, index) => {
const year = String(current - index)
return { value: year, label: year }
})
}, [])
const dayOptions = useMemo(
() => Array.from({ length: 31 }, (_, i) => ({ value: String(i + 1), label: String(i + 1) })),
[],
)
const clearSectionStatus = (section) => {
setSavedMessage((prev) => (prev.section === section ? { section: '', text: '' } : prev))
setErrorsBySection((prev) => ({ ...prev, [section]: {} }))
}
const resetCaptchaState = () => {
setCaptchaState((prev) => ({
...prev,
required: false,
section: '',
token: '',
message: '',
nonce: prev.nonce + 1,
}))
}
const captureCaptchaRequirement = (section, payload = {}) => {
const requiresCaptcha = !!(payload?.requires_captcha || payload?.requiresCaptcha)
if (!requiresCaptcha) {
return false
}
const nextCaptcha = payload?.captcha || {}
const message = payload?.errors?.captcha?.[0] || payload?.message || 'Complete the captcha challenge to continue.'
setCaptchaState((prev) => ({
required: true,
section,
token: '',
message,
nonce: prev.nonce + 1,
provider: nextCaptcha.provider || payload?.captcha_provider || prev.provider || initialCaptcha?.provider || 'turnstile',
siteKey: nextCaptcha.siteKey || payload?.captcha_site_key || prev.siteKey || initialCaptcha?.siteKey || '',
inputName: nextCaptcha.inputName || payload?.captcha_input || prev.inputName || initialCaptcha?.inputName || 'cf-turnstile-response',
scriptUrl: nextCaptcha.scriptUrl || payload?.captcha_script_url || prev.scriptUrl || initialCaptcha?.scriptUrl || '',
}))
updateSectionErrors(section, {
_general: [message],
captcha: [message],
})
return true
}
const applyCaptchaPayload = (payload = {}) => {
if (!captchaState.required || !captchaState.inputName) {
return payload
}
return {
...payload,
[captchaState.inputName]: captchaState.token || '',
}
}
const applyCaptchaFormData = (formData) => {
if (captchaState.required && captchaState.inputName) {
formData.set(captchaState.inputName, captchaState.token || '')
}
}
const renderCaptchaChallenge = (section, placement = 'section') => {
if (!captchaState.required || !captchaState.siteKey || activeSection !== section) {
return null
}
if (section === 'account' && showEmailChangeModal && placement !== 'modal') {
return null
}
if (section === 'account' && !showEmailChangeModal && placement === 'modal') {
return null
}
return (
{captchaState.message || 'Complete the captcha challenge to continue.'}
setCaptchaState((prev) => ({ ...prev, token }))}
className="rounded-lg border border-white/10 bg-black/20 p-3"
/>
)
}
const switchSection = (nextSection) => {
if (activeSection === nextSection) return
if (dirtyMap[activeSection]) {
const shouldContinue = window.confirm('You have unsaved changes in this section. Leave without saving?')
if (!shouldContinue) return
}
setActiveSection(nextSection)
}
const handleAvatarSelect = (file) => {
if (!file) return
if (!file.type.startsWith('image/')) return
setAvatarFile(file)
setRemoveAvatar(false)
setAvatarUrl(URL.createObjectURL(file))
clearSectionStatus('profile')
}
const dragHandler = (event) => {
event.preventDefault()
event.stopPropagation()
}
const handleDrop = (event) => {
dragHandler(event)
setDragActive(false)
const file = event.dataTransfer?.files?.[0]
if (file) handleAvatarSelect(file)
}
const updateSectionErrors = (section, errors = {}) => {
setErrorsBySection((prev) => ({ ...prev, [section]: errors }))
}
const saveProfileSection = async (event) => {
event.preventDefault()
setSavingSection('profile')
clearSectionStatus('profile')
try {
const formData = new FormData()
formData.append('display_name', profileForm.display_name || '')
formData.append('website', profileForm.website || '')
formData.append('bio', profileForm.bio || '')
formData.append('signature', profileForm.signature || '')
formData.append('description', profileForm.description || '')
formData.append('remove_avatar', removeAvatar ? '1' : '0')
formData.append('avatar_position', avatarPosition)
if (avatarFile) {
formData.append('avatar', avatarFile)
}
applyCaptchaFormData(formData)
const response = await fetch('/settings/profile/update', {
method: 'POST',
credentials: 'same-origin',
headers: await botHeaders({
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}, captchaState),
body: formData,
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
if (captureCaptchaRequirement('profile', payload)) {
return
}
updateSectionErrors('profile', payload.errors || { _general: [payload.message || 'Unable to save profile section.'] })
return
}
const nextAvatarUrl = payload.avatarUrl || avatarUrl || initialAvatarUrl
initialRef.current.profileForm = { ...profileForm }
initialRef.current.avatarUrl = nextAvatarUrl || ''
setAvatarUrl(nextAvatarUrl || '')
setAvatarFile(null)
setAvatarPosition('center')
setRemoveAvatar(false)
resetCaptchaState()
setSavedMessage({ section: 'profile', text: payload.message || 'Profile updated successfully.' })
} catch (error) {
updateSectionErrors('profile', { _general: ['Request failed. Please try again.'] })
} finally {
setSavingSection('')
}
}
const saveAccountSection = async (event) => {
event.preventDefault()
if (usernameCooldownActive && accountForm.username !== initialRef.current.accountForm.username) {
updateSectionErrors('account', {
username: [`Username can be changed again in ${usernameCooldownRemainingDays} days.`],
})
return
}
setSavingSection('account')
clearSectionStatus('account')
try {
const response = await fetch('/settings/account/username', {
method: 'POST',
credentials: 'same-origin',
headers: await botHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}, captchaState),
body: JSON.stringify(applyCaptchaPayload({ username: accountForm.username, homepage_url: '' })),
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
if (captureCaptchaRequirement('account', payload)) {
return
}
updateSectionErrors('account', payload.errors || { _general: [payload.message || 'Unable to save account section.'] })
return
}
initialRef.current.accountForm = { ...accountForm }
resetCaptchaState()
setSavedMessage({ section: 'account', text: payload.message || 'Account updated successfully.' })
} catch (error) {
updateSectionErrors('account', { _general: ['Request failed. Please try again.'] })
} finally {
setSavingSection('')
}
}
const requestEmailChange = async () => {
setEmailChangeLoading(true)
setEmailChangeError('')
setEmailChangeInfo('')
try {
const response = await fetch('/settings/email/request', {
method: 'POST',
credentials: 'same-origin',
headers: await botHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}, captchaState),
body: JSON.stringify(applyCaptchaPayload({ new_email: emailChangeForm.new_email, homepage_url: '' })),
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
if (captureCaptchaRequirement('account', payload)) {
setEmailChangeError(payload?.errors?.captcha?.[0] || payload?.message || 'Complete the captcha challenge to continue.')
return
}
setEmailChangeError(payload?.errors?.new_email?.[0] || payload?.message || 'Unable to request email change.')
return
}
setEmailChangeStep('verify')
resetCaptchaState()
setEmailChangeInfo(payload.message || 'Verification code sent to your new email address.')
} catch (error) {
setEmailChangeError('Request failed. Please try again.')
} finally {
setEmailChangeLoading(false)
}
}
const verifyEmailChange = async () => {
setEmailChangeLoading(true)
setEmailChangeError('')
setEmailChangeInfo('')
try {
const response = await fetch('/settings/email/verify', {
method: 'POST',
credentials: 'same-origin',
headers: await botHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}, captchaState),
body: JSON.stringify(applyCaptchaPayload({ code: emailChangeForm.code, homepage_url: '' })),
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
if (captureCaptchaRequirement('account', payload)) {
setEmailChangeError(payload?.errors?.captcha?.[0] || payload?.message || 'Complete the captcha challenge to continue.')
return
}
setEmailChangeError(payload?.errors?.code?.[0] || payload?.message || 'Verification failed.')
return
}
const nextEmail = payload.email || emailChangeForm.new_email
setAccountForm((prev) => ({ ...prev, email: nextEmail }))
initialRef.current.accountForm = { ...initialRef.current.accountForm, email: nextEmail }
setShowEmailChangeModal(false)
setEmailChangeStep('request')
setEmailChangeForm({ new_email: '', code: '' })
resetCaptchaState()
setSavedMessage({ section: 'account', text: payload.message || 'Email updated successfully.' })
} catch (error) {
setEmailChangeError('Request failed. Please try again.')
} finally {
setEmailChangeLoading(false)
}
}
const savePersonalSection = async (event) => {
event.preventDefault()
setSavingSection('personal')
clearSectionStatus('personal')
try {
const response = await fetch('/settings/personal/update', {
method: 'POST',
credentials: 'same-origin',
headers: await botHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}, captchaState),
body: JSON.stringify(applyCaptchaPayload({
birthday: toIsoDate(personalForm.day, personalForm.month, personalForm.year) || null,
gender: personalForm.gender || null,
country_id: personalForm.country_id || null,
homepage_url: '',
})),
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
if (captureCaptchaRequirement('personal', payload)) {
return
}
updateSectionErrors('personal', payload.errors || { _general: [payload.message || 'Unable to save personal details.'] })
return
}
initialRef.current.personalForm = { ...personalForm }
resetCaptchaState()
setSavedMessage({ section: 'personal', text: payload.message || 'Personal details saved successfully.' })
} catch (error) {
updateSectionErrors('personal', { _general: ['Request failed. Please try again.'] })
} finally {
setSavingSection('')
}
}
const saveNotificationsSection = async (event) => {
event.preventDefault()
setSavingSection('notifications')
clearSectionStatus('notifications')
try {
const response = await fetch('/settings/notifications/update', {
method: 'POST',
credentials: 'same-origin',
headers: await botHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}, captchaState),
body: JSON.stringify(applyCaptchaPayload({ ...notificationForm, homepage_url: '' })),
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
if (captureCaptchaRequirement('notifications', payload)) {
return
}
updateSectionErrors('notifications', payload.errors || { _general: [payload.message || 'Unable to save notifications.'] })
return
}
initialRef.current.notificationForm = { ...notificationForm }
resetCaptchaState()
setSavedMessage({ section: 'notifications', text: payload.message || 'Notification settings saved successfully.' })
} catch (error) {
updateSectionErrors('notifications', { _general: ['Request failed. Please try again.'] })
} finally {
setSavingSection('')
}
}
const saveSecuritySection = async (event) => {
event.preventDefault()
setSavingSection('security')
clearSectionStatus('security')
try {
const response = await fetch('/settings/security/password', {
method: 'POST',
credentials: 'same-origin',
headers: await botHeaders({
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
}, captchaState),
body: JSON.stringify(applyCaptchaPayload({ ...securityForm, homepage_url: '' })),
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
if (captureCaptchaRequirement('security', payload)) {
return
}
updateSectionErrors('security', payload.errors || { _general: [payload.message || 'Unable to update password.'] })
return
}
setSecurityForm({
current_password: '',
new_password: '',
new_password_confirmation: '',
})
resetCaptchaState()
setSavedMessage({ section: 'security', text: payload.message || 'Password updated successfully.' })
} catch (error) {
updateSectionErrors('security', { _general: ['Request failed. Please try again.'] })
} finally {
setSavingSection('')
}
}
const handleDeleteAccount = async () => {
setDeleting(true)
setDeleteError('')
try {
const response = await fetch('/profile', {
method: 'DELETE',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'X-CSRF-TOKEN': getCsrfToken(),
},
body: JSON.stringify({ password: deletePassword }),
})
if (response.ok || response.status === 302) {
window.location.href = '/'
return
}
const payload = await response.json().catch(() => ({}))
setDeleteError(payload.errors?.password?.[0] || payload.message || 'Deletion failed.')
} catch (error) {
setDeleteError('Request failed.')
} finally {
setDeleting(false)
}
}
const sectionSaved = savedMessage.section === activeSection ? savedMessage.text : ''
return (
Configure your account by section. Each card saves independently.
{dirtyMap[activeSection] ? (
Unsaved changes
) : null}
{activeSection === 'profile' ? (
) : null}
{activeSection === 'account' ? (
) : null}
{activeSection === 'personal' ? (
) : null}
{activeSection === 'notifications' ? (
) : null}
{activeSection === 'security' ? (
) : null}
{activeSection === 'danger' ? (
setShowDeleteModal(true)}>
Delete Account
}
>
Deleting your account permanently removes your artworks, comments, and profile data.
) : null}
{emailChangeStep === 'request' ? (
) : (
)}
}
>
{emailChangeError ? (
{emailChangeError}
) : null}
{emailChangeInfo ? (
{emailChangeInfo}
) : null}
{renderCaptchaChallenge('account', 'modal')}
{emailChangeStep === 'request' ? (
setEmailChangeForm((prev) => ({ ...prev, new_email: e.target.value }))}
autoComplete="email"
required
/>
) : (
<>
Enter the 6-digit verification code sent to {emailChangeForm.new_email}.
setEmailChangeForm((prev) => ({ ...prev, code: e.target.value }))}
inputMode="numeric"
maxLength={6}
required
/>
>
)}
setShowDeleteModal(false)}
title="Delete Account"
size="sm"
footer={
}
>
Confirm your password to permanently delete your account.
setDeletePassword(e.target.value)}
error={deleteError}
autoComplete="current-password"
/>
)
}