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 (
{children}
) } function SectionTitle({ icon, children, description }) { return (

{icon && } {children}

{description &&

{description}

}
) } // ─── 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 (
{/* ── General Errors ── */} {profileErrors._general && (
{profileErrors._general[0]}
)} {/* ════════════════════════════════════════════════════════════════════ AVATAR SECTION ════════════════════════════════════════════════════════════════════ */}
Avatar
{/* Preview */}
{username {avatarUploading && (
)}
{/* Dropzone */}
handleAvatarSelect(e.target.files?.[0])} /> {avatarFile && (
)}
{/* ════════════════════════════════════════════════════════════════════ ACCOUNT INFO ════════════════════════════════════════════════════════════════════ */}
Account
setUsername(e.target.value)} error={profileErrors.username?.[0]} hint={user?.username_changed_at ? `Last changed: ${new Date(user.username_changed_at).toLocaleDateString()}` : undefined} /> setEmail(e.target.value)} error={profileErrors.email?.[0]} required /> setName(e.target.value)} placeholder="Your real name (optional)" error={profileErrors.name?.[0]} /> setHomepage(e.target.value)} placeholder="https://" error={profileErrors.web?.[0] || profileErrors.homepage?.[0]} />
{/* ════════════════════════════════════════════════════════════════════ ABOUT & BIO ════════════════════════════════════════════════════════════════════ */}
About & Bio