728 lines
30 KiB
JavaScript
728 lines
30 KiB
JavaScript
import React, { useState, useRef, useCallback } 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 Modal from '../../components/ui/Modal'
|
||
import { RadioGroup } from '../../components/ui/Radio'
|
||
|
||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||
|
||
function getCsrfToken() {
|
||
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||
}
|
||
|
||
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' },
|
||
]
|
||
|
||
function buildDayOptions() {
|
||
return Array.from({ length: 31 }, (_, i) => ({ value: String(i + 1), label: String(i + 1) }))
|
||
}
|
||
|
||
function buildYearOptions() {
|
||
const currentYear = new Date().getFullYear()
|
||
const years = []
|
||
for (let y = currentYear; y >= currentYear - 100; y--) {
|
||
years.push({ value: String(y), label: String(y) })
|
||
}
|
||
return years
|
||
}
|
||
|
||
// ─── Sub-components ──────────────────────────────────────────────────────────
|
||
|
||
function Section({ children, className = '' }) {
|
||
return (
|
||
<section className={`bg-nova-900/60 border border-white/10 rounded-2xl p-6 ${className}`}>
|
||
{children}
|
||
</section>
|
||
)
|
||
}
|
||
|
||
function SectionTitle({ icon, children, description }) {
|
||
return (
|
||
<div className="mb-5">
|
||
<h3 className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-slate-400">
|
||
{icon && <i className={`${icon} text-accent/70 text-[11px]`} />}
|
||
{children}
|
||
</h3>
|
||
{description && <p className="text-xs text-slate-500 mt-1">{description}</p>}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// ─── Main Component ──────────────────────────────────────────────────────────
|
||
|
||
export default function ProfileEdit() {
|
||
const { props } = usePage()
|
||
const {
|
||
user,
|
||
avatarUrl: initialAvatarUrl,
|
||
birthDay: initDay,
|
||
birthMonth: initMonth,
|
||
birthYear: initYear,
|
||
countries = [],
|
||
flash = {},
|
||
} = props
|
||
|
||
// ── Profile State ──────────────────────────────────────────────────────────
|
||
const [name, setName] = useState(user?.name || '')
|
||
const [email, setEmail] = useState(user?.email || '')
|
||
const [username, setUsername] = useState(user?.username || '')
|
||
const [homepage, setHomepage] = useState(user?.homepage || user?.website || '')
|
||
const [about, setAbout] = useState(user?.about_me || user?.about || '')
|
||
const [signature, setSignature] = useState(user?.signature || '')
|
||
const [description, setDescription] = useState(user?.description || '')
|
||
const [day, setDay] = useState(initDay ? String(parseInt(initDay, 10)) : '')
|
||
const [month, setMonth] = useState(initMonth ? String(parseInt(initMonth, 10)) : '')
|
||
const [year, setYear] = useState(initYear ? String(initYear) : '')
|
||
const [gender, setGender] = useState(() => {
|
||
const g = (user?.gender || '').toLowerCase()
|
||
if (g === 'm') return 'm'
|
||
if (g === 'f') return 'f'
|
||
if (g === 'x' || g === 'n') return 'x'
|
||
return ''
|
||
})
|
||
const [country, setCountry] = useState(user?.country_code || user?.country || '')
|
||
const [mailing, setMailing] = useState(!!user?.mlist)
|
||
const [notify, setNotify] = useState(!!user?.friend_upload_notice)
|
||
const [autoPost, setAutoPost] = useState(!!user?.auto_post_upload)
|
||
|
||
// Avatar
|
||
const [avatarUrl, setAvatarUrl] = useState(initialAvatarUrl || '')
|
||
const [avatarFile, setAvatarFile] = useState(null)
|
||
const [avatarUploading, setAvatarUploading] = useState(false)
|
||
const avatarInputRef = useRef(null)
|
||
const [dragActive, setDragActive] = useState(false)
|
||
|
||
// Save state
|
||
const [saving, setSaving] = useState(false)
|
||
const [profileErrors, setProfileErrors] = useState({})
|
||
const [profileSaved, setProfileSaved] = useState(!!flash?.status)
|
||
|
||
// Password state
|
||
const [currentPassword, setCurrentPassword] = useState('')
|
||
const [newPassword, setNewPassword] = useState('')
|
||
const [confirmPassword, setConfirmPassword] = useState('')
|
||
const [passwordSaving, setPasswordSaving] = useState(false)
|
||
const [passwordErrors, setPasswordErrors] = useState({})
|
||
const [passwordSaved, setPasswordSaved] = useState(false)
|
||
|
||
// Delete account
|
||
const [showDelete, setShowDelete] = useState(false)
|
||
const [deletePassword, setDeletePassword] = useState('')
|
||
const [deleting, setDeleting] = useState(false)
|
||
const [deleteError, setDeleteError] = useState('')
|
||
|
||
// ── Country Options ─────────────────────────────────────────────────────────
|
||
const countryOptions = (countries || []).map((c) => ({
|
||
value: c.country_code || c.code || c.id || '',
|
||
label: c.country_name || c.name || '',
|
||
}))
|
||
|
||
// ── Avatar Handlers ────────────────────────────────────────────────────────
|
||
const handleAvatarSelect = (file) => {
|
||
if (!file || !file.type.startsWith('image/')) return
|
||
setAvatarFile(file)
|
||
setAvatarUrl(URL.createObjectURL(file))
|
||
}
|
||
|
||
const handleAvatarUpload = useCallback(async () => {
|
||
if (!avatarFile) return
|
||
setAvatarUploading(true)
|
||
try {
|
||
const fd = new FormData()
|
||
fd.append('avatar', avatarFile)
|
||
const res = await fetch('/avatar/upload', {
|
||
method: 'POST',
|
||
headers: { Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
||
credentials: 'same-origin',
|
||
body: fd,
|
||
})
|
||
const data = await res.json()
|
||
if (res.ok && data.url) {
|
||
setAvatarUrl(data.url)
|
||
setAvatarFile(null)
|
||
}
|
||
} catch (err) {
|
||
console.error('Avatar upload failed:', err)
|
||
} finally {
|
||
setAvatarUploading(false)
|
||
}
|
||
}, [avatarFile])
|
||
|
||
const handleDrag = (e) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
}
|
||
|
||
const handleDragIn = (e) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
setDragActive(true)
|
||
}
|
||
|
||
const handleDragOut = (e) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
setDragActive(false)
|
||
}
|
||
|
||
const handleDrop = (e) => {
|
||
e.preventDefault()
|
||
e.stopPropagation()
|
||
setDragActive(false)
|
||
const file = e.dataTransfer?.files?.[0]
|
||
if (file) handleAvatarSelect(file)
|
||
}
|
||
|
||
// ── Profile Save ───────────────────────────────────────────────────────────
|
||
const handleProfileSave = useCallback(async () => {
|
||
setSaving(true)
|
||
setProfileSaved(false)
|
||
setProfileErrors({})
|
||
try {
|
||
const fd = new FormData()
|
||
fd.append('_method', 'PUT')
|
||
fd.append('email', email)
|
||
fd.append('username', username)
|
||
fd.append('name', name)
|
||
if (homepage) fd.append('web', homepage)
|
||
if (about) fd.append('about', about)
|
||
if (signature) fd.append('signature', signature)
|
||
if (description) fd.append('description', description)
|
||
if (day) fd.append('day', day)
|
||
if (month) fd.append('month', month)
|
||
if (year) fd.append('year', year)
|
||
if (gender) fd.append('gender', gender)
|
||
if (country) fd.append('country', country)
|
||
fd.append('mailing', mailing ? '1' : '0')
|
||
fd.append('notify', notify ? '1' : '0')
|
||
fd.append('auto_post_upload', autoPost ? '1' : '0')
|
||
if (avatarFile) fd.append('avatar', avatarFile)
|
||
|
||
const res = await fetch('/profile', {
|
||
method: 'POST',
|
||
headers: { Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
||
credentials: 'same-origin',
|
||
body: fd,
|
||
})
|
||
|
||
if (res.ok || res.status === 302) {
|
||
setProfileSaved(true)
|
||
setAvatarFile(null)
|
||
setTimeout(() => setProfileSaved(false), 4000)
|
||
} else {
|
||
const data = await res.json().catch(() => ({}))
|
||
if (data.errors) setProfileErrors(data.errors)
|
||
else if (data.message) setProfileErrors({ _general: [data.message] })
|
||
}
|
||
} catch (err) {
|
||
console.error('Profile save failed:', err)
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}, [email, username, name, homepage, about, signature, description, day, month, year, gender, country, mailing, notify, autoPost, avatarFile])
|
||
|
||
// ── Password Change ────────────────────────────────────────────────────────
|
||
const handlePasswordChange = useCallback(async () => {
|
||
setPasswordSaving(true)
|
||
setPasswordSaved(false)
|
||
setPasswordErrors({})
|
||
try {
|
||
const res = await fetch('/profile/password', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
||
credentials: 'same-origin',
|
||
body: JSON.stringify({
|
||
current_password: currentPassword,
|
||
password: newPassword,
|
||
password_confirmation: confirmPassword,
|
||
}),
|
||
})
|
||
|
||
if (res.ok || res.status === 302) {
|
||
setPasswordSaved(true)
|
||
setCurrentPassword('')
|
||
setNewPassword('')
|
||
setConfirmPassword('')
|
||
setTimeout(() => setPasswordSaved(false), 4000)
|
||
} else {
|
||
const data = await res.json().catch(() => ({}))
|
||
if (data.errors) setPasswordErrors(data.errors)
|
||
else if (data.message) setPasswordErrors({ _general: [data.message] })
|
||
}
|
||
} catch (err) {
|
||
console.error('Password change failed:', err)
|
||
} finally {
|
||
setPasswordSaving(false)
|
||
}
|
||
}, [currentPassword, newPassword, confirmPassword])
|
||
|
||
// ── Delete Account ─────────────────────────────────────────────────────────
|
||
const handleDeleteAccount = async () => {
|
||
setDeleting(true)
|
||
setDeleteError('')
|
||
try {
|
||
const res = await fetch('/profile', {
|
||
method: 'DELETE',
|
||
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-CSRF-TOKEN': getCsrfToken() },
|
||
credentials: 'same-origin',
|
||
body: JSON.stringify({ password: deletePassword }),
|
||
})
|
||
if (res.ok || res.status === 302) {
|
||
window.location.href = '/'
|
||
} else {
|
||
const data = await res.json().catch(() => ({}))
|
||
setDeleteError(data.errors?.password?.[0] || data.message || 'Deletion failed.')
|
||
}
|
||
} catch (err) {
|
||
setDeleteError('Request failed.')
|
||
} finally {
|
||
setDeleting(false)
|
||
}
|
||
}
|
||
|
||
// ── Render ─────────────────────────────────────────────────────────────────
|
||
return (
|
||
<SettingsLayout title="Edit Profile">
|
||
<div className="space-y-8">
|
||
|
||
{/* ── General Errors ── */}
|
||
{profileErrors._general && (
|
||
<div className="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-300">
|
||
{profileErrors._general[0]}
|
||
</div>
|
||
)}
|
||
|
||
{/* ════════════════════════════════════════════════════════════════════
|
||
AVATAR SECTION
|
||
════════════════════════════════════════════════════════════════════ */}
|
||
<Section>
|
||
<SectionTitle icon="fa-solid fa-camera" description="JPG, PNG or WebP. Max 2 MB.">
|
||
Avatar
|
||
</SectionTitle>
|
||
|
||
<div className="flex items-center gap-6">
|
||
{/* Preview */}
|
||
<div className="relative shrink-0">
|
||
<img
|
||
src={avatarUrl}
|
||
alt={username || 'Avatar'}
|
||
className="w-24 h-24 rounded-full object-cover ring-2 ring-white/10 shadow-lg"
|
||
/>
|
||
{avatarUploading && (
|
||
<div className="absolute inset-0 rounded-full bg-black/60 flex items-center justify-center">
|
||
<div className="w-6 h-6 border-2 border-accent/30 border-t-accent rounded-full animate-spin" />
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Dropzone */}
|
||
<div className="flex-1 min-w-0">
|
||
<button
|
||
type="button"
|
||
onClick={() => avatarInputRef.current?.click()}
|
||
onDragEnter={handleDragIn}
|
||
onDragLeave={handleDragOut}
|
||
onDragOver={handleDrag}
|
||
onDrop={handleDrop}
|
||
className={[
|
||
'w-full rounded-xl border-2 border-dashed px-5 py-5 text-left transition-all',
|
||
dragActive
|
||
? 'border-accent/50 bg-accent/10'
|
||
: 'border-white/15 hover:border-white/25 hover:bg-white/[0.03]',
|
||
].join(' ')}
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<span className="flex items-center justify-center w-10 h-10 rounded-lg bg-white/5 text-slate-400">
|
||
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||
<path d="M5.5 13a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 0113.5 13H11V9.414l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 101.414 1.414L9 9.414V13H5.5z" />
|
||
<path d="M9 13h2v5a1 1 0 11-2 0v-5z" />
|
||
</svg>
|
||
</span>
|
||
<div className="min-w-0">
|
||
<p className="text-sm text-white/90 font-medium">
|
||
{avatarFile ? avatarFile.name : 'Drop an image or click to browse'}
|
||
</p>
|
||
<p className="text-[11px] text-slate-500 mt-0.5">
|
||
{avatarFile ? 'Ready to upload with save' : 'Recommended: 256×256 px or larger'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
<input
|
||
ref={avatarInputRef}
|
||
type="file"
|
||
className="hidden"
|
||
accept="image/jpeg,image/png,image/webp"
|
||
onChange={(e) => handleAvatarSelect(e.target.files?.[0])}
|
||
/>
|
||
{avatarFile && (
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<Button variant="accent" size="xs" loading={avatarUploading} onClick={handleAvatarUpload}>
|
||
Upload now
|
||
</Button>
|
||
<button
|
||
type="button"
|
||
onClick={() => { setAvatarFile(null); setAvatarUrl(initialAvatarUrl || '') }}
|
||
className="text-[11px] text-slate-500 hover:text-white transition-colors"
|
||
>
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* ════════════════════════════════════════════════════════════════════
|
||
ACCOUNT INFO
|
||
════════════════════════════════════════════════════════════════════ */}
|
||
<Section>
|
||
<SectionTitle icon="fa-solid fa-user" description="Your public identity on Skinbase.">
|
||
Account
|
||
</SectionTitle>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||
<TextInput
|
||
label="Username"
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
error={profileErrors.username?.[0]}
|
||
hint={user?.username_changed_at ? `Last changed: ${new Date(user.username_changed_at).toLocaleDateString()}` : undefined}
|
||
/>
|
||
<TextInput
|
||
label="Email"
|
||
type="email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
error={profileErrors.email?.[0]}
|
||
required
|
||
/>
|
||
<TextInput
|
||
label="Display Name"
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
placeholder="Your real name (optional)"
|
||
error={profileErrors.name?.[0]}
|
||
/>
|
||
<TextInput
|
||
label="Website"
|
||
type="url"
|
||
value={homepage}
|
||
onChange={(e) => setHomepage(e.target.value)}
|
||
placeholder="https://"
|
||
error={profileErrors.web?.[0] || profileErrors.homepage?.[0]}
|
||
/>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* ════════════════════════════════════════════════════════════════════
|
||
ABOUT & BIO
|
||
════════════════════════════════════════════════════════════════════ */}
|
||
<Section>
|
||
<SectionTitle icon="fa-solid fa-pen-fancy" description="Tell the community about yourself.">
|
||
About & Bio
|
||
</SectionTitle>
|
||
|
||
<div className="space-y-5">
|
||
<Textarea
|
||
label="About Me"
|
||
value={about}
|
||
onChange={(e) => setAbout(e.target.value)}
|
||
placeholder="Share something about yourself…"
|
||
rows={4}
|
||
error={profileErrors.about?.[0]}
|
||
/>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||
<Textarea
|
||
label="Signature"
|
||
value={signature}
|
||
onChange={(e) => setSignature(e.target.value)}
|
||
placeholder="Forum signature"
|
||
rows={3}
|
||
error={profileErrors.signature?.[0]}
|
||
/>
|
||
<Textarea
|
||
label="Description"
|
||
value={description}
|
||
onChange={(e) => setDescription(e.target.value)}
|
||
placeholder="Short bio / tagline"
|
||
rows={3}
|
||
error={profileErrors.description?.[0]}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* ════════════════════════════════════════════════════════════════════
|
||
PERSONAL DETAILS
|
||
════════════════════════════════════════════════════════════════════ */}
|
||
<Section>
|
||
<SectionTitle icon="fa-solid fa-id-card" description="Optional details — only shown if you choose.">
|
||
Personal Details
|
||
</SectionTitle>
|
||
|
||
<div className="space-y-5">
|
||
{/* Birthday */}
|
||
<div>
|
||
<label className="text-sm font-medium text-white/85 block mb-1.5">Birthday</label>
|
||
<div className="grid grid-cols-3 gap-3 max-w-md">
|
||
<Select
|
||
placeholder="Day"
|
||
value={day}
|
||
onChange={(e) => setDay(e.target.value)}
|
||
options={buildDayOptions()}
|
||
size="sm"
|
||
/>
|
||
<Select
|
||
placeholder="Month"
|
||
value={month}
|
||
onChange={(e) => setMonth(e.target.value)}
|
||
options={MONTHS}
|
||
size="sm"
|
||
/>
|
||
<Select
|
||
placeholder="Year"
|
||
value={year}
|
||
onChange={(e) => setYear(e.target.value)}
|
||
options={buildYearOptions()}
|
||
size="sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Gender */}
|
||
<RadioGroup
|
||
label="Gender"
|
||
name="gender"
|
||
options={GENDER_OPTIONS}
|
||
value={gender}
|
||
onChange={setGender}
|
||
direction="horizontal"
|
||
error={profileErrors.gender?.[0]}
|
||
/>
|
||
|
||
{/* Country */}
|
||
{countryOptions.length > 0 ? (
|
||
<Select
|
||
label="Country"
|
||
value={country}
|
||
onChange={(e) => setCountry(e.target.value)}
|
||
options={countryOptions}
|
||
placeholder="Select country"
|
||
error={profileErrors.country?.[0]}
|
||
/>
|
||
) : (
|
||
<TextInput
|
||
label="Country"
|
||
value={country}
|
||
onChange={(e) => setCountry(e.target.value)}
|
||
placeholder="Country code (e.g. US, DE, TR)"
|
||
error={profileErrors.country?.[0]}
|
||
/>
|
||
)}
|
||
</div>
|
||
</Section>
|
||
|
||
{/* ════════════════════════════════════════════════════════════════════
|
||
PREFERENCES
|
||
════════════════════════════════════════════════════════════════════ */}
|
||
<Section>
|
||
<SectionTitle icon="fa-solid fa-sliders" description="Control emails and sharing behavior.">
|
||
Preferences
|
||
</SectionTitle>
|
||
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between py-1">
|
||
<div>
|
||
<p className="text-sm text-white/90 font-medium">Mailing List</p>
|
||
<p className="text-xs text-slate-500">Receive occasional emails about Skinbase news</p>
|
||
</div>
|
||
<Toggle
|
||
checked={mailing}
|
||
onChange={(e) => setMailing(e.target.checked)}
|
||
variant="accent"
|
||
size="md"
|
||
/>
|
||
</div>
|
||
|
||
<div className="border-t border-white/5" />
|
||
|
||
<div className="flex items-center justify-between py-1">
|
||
<div>
|
||
<p className="text-sm text-white/90 font-medium">Upload Notifications</p>
|
||
<p className="text-xs text-slate-500">Get notified when people you follow upload new work</p>
|
||
</div>
|
||
<Toggle
|
||
checked={notify}
|
||
onChange={(e) => setNotify(e.target.checked)}
|
||
variant="emerald"
|
||
size="md"
|
||
/>
|
||
</div>
|
||
|
||
<div className="border-t border-white/5" />
|
||
|
||
<div className="flex items-center justify-between py-1">
|
||
<div>
|
||
<p className="text-sm text-white/90 font-medium">Auto-post Uploads</p>
|
||
<p className="text-xs text-slate-500">Automatically post to your feed when you publish artwork</p>
|
||
</div>
|
||
<Toggle
|
||
checked={autoPost}
|
||
onChange={(e) => setAutoPost(e.target.checked)}
|
||
variant="sky"
|
||
size="md"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* ── Save Profile Button ── */}
|
||
<div className="flex items-center gap-3">
|
||
<Button variant="accent" size="md" loading={saving} onClick={handleProfileSave}>
|
||
Save Profile
|
||
</Button>
|
||
{profileSaved && (
|
||
<span className="text-sm text-emerald-400 flex items-center gap-1.5 animate-pulse">
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||
<path fillRule="evenodd" d="M8 16A8 8 0 108 0a8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L7 8.586 5.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
Profile updated
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* ════════════════════════════════════════════════════════════════════
|
||
CHANGE PASSWORD
|
||
════════════════════════════════════════════════════════════════════ */}
|
||
<Section className="mt-4">
|
||
<SectionTitle icon="fa-solid fa-lock" description="Use a strong, unique password.">
|
||
Change Password
|
||
</SectionTitle>
|
||
|
||
{passwordErrors._general && (
|
||
<div className="rounded-xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-300 mb-4">
|
||
{passwordErrors._general[0]}
|
||
</div>
|
||
)}
|
||
|
||
<div className="space-y-4 max-w-md">
|
||
<TextInput
|
||
label="Current Password"
|
||
type="password"
|
||
value={currentPassword}
|
||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||
error={passwordErrors.current_password?.[0]}
|
||
autoComplete="current-password"
|
||
/>
|
||
<TextInput
|
||
label="New Password"
|
||
type="password"
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
error={passwordErrors.password?.[0]}
|
||
hint="Minimum 8 characters"
|
||
autoComplete="new-password"
|
||
/>
|
||
<TextInput
|
||
label="Confirm New Password"
|
||
type="password"
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
error={passwordErrors.password_confirmation?.[0]}
|
||
autoComplete="new-password"
|
||
/>
|
||
|
||
<div className="flex items-center gap-3 pt-1">
|
||
<Button variant="secondary" size="md" loading={passwordSaving} onClick={handlePasswordChange}>
|
||
Update Password
|
||
</Button>
|
||
{passwordSaved && (
|
||
<span className="text-sm text-emerald-400 flex items-center gap-1.5 animate-pulse">
|
||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||
<path fillRule="evenodd" d="M8 16A8 8 0 108 0a8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L7 8.586 5.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||
</svg>
|
||
Password updated
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* ════════════════════════════════════════════════════════════════════
|
||
DANGER ZONE
|
||
════════════════════════════════════════════════════════════════════ */}
|
||
<Section className="border-red-500/20">
|
||
<SectionTitle icon="fa-solid fa-triangle-exclamation" description="Permanent actions that cannot be undone.">
|
||
Danger Zone
|
||
</SectionTitle>
|
||
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<p className="text-sm text-white/90 font-medium">Delete Account</p>
|
||
<p className="text-xs text-slate-500">Remove your account and all associated data permanently.</p>
|
||
</div>
|
||
<Button variant="danger" size="sm" onClick={() => setShowDelete(true)}>
|
||
Delete Account
|
||
</Button>
|
||
</div>
|
||
</Section>
|
||
|
||
{/* spacer for bottom padding */}
|
||
<div className="h-4" />
|
||
</div>
|
||
|
||
{/* ── Delete Confirmation Modal ── */}
|
||
<Modal
|
||
open={showDelete}
|
||
onClose={() => setShowDelete(false)}
|
||
title="Delete Account"
|
||
size="sm"
|
||
footer={
|
||
<div className="flex items-center gap-3 ml-auto">
|
||
<Button variant="ghost" size="sm" onClick={() => setShowDelete(false)}>
|
||
Cancel
|
||
</Button>
|
||
<Button variant="danger" size="sm" loading={deleting} onClick={handleDeleteAccount}>
|
||
Permanently Delete
|
||
</Button>
|
||
</div>
|
||
}
|
||
>
|
||
<div className="space-y-4">
|
||
<p className="text-sm text-slate-300">
|
||
This action is <span className="text-red-400 font-semibold">irreversible</span>. All your artworks,
|
||
comments, and profile data will be permanently deleted.
|
||
</p>
|
||
<TextInput
|
||
label="Confirm your password"
|
||
type="password"
|
||
value={deletePassword}
|
||
onChange={(e) => setDeletePassword(e.target.value)}
|
||
error={deleteError}
|
||
autoComplete="current-password"
|
||
/>
|
||
</div>
|
||
</Modal>
|
||
</SettingsLayout>
|
||
)
|
||
}
|