Profile image works, editing profile works, now uses first/last name

This commit is contained in:
augmentedpotato
2026-04-14 15:02:57 -06:00
parent c5448b95c9
commit 2282f0da6f
7 changed files with 212 additions and 53 deletions

View File

@@ -33,7 +33,8 @@ export default function ProfilePage() {
const [petAge, setPetAge] = useState("1");
const [submitting, setSubmitting] = useState(false);
const [petError, setPetError] = useState(null);
const [profileForm, setProfileForm] = useState({ fullName: "", email: "", phone: "" });
const [avatarObjectUrl, setAvatarObjectUrl] = useState(null);
const [profileForm, setProfileForm] = useState({ firstName: "", lastName: "", email: "", phone: "", password: "", confirmPassword: "" });
const [profileSubmitting, setProfileSubmitting] = useState(false);
const [profileError, setProfileError] = useState(null);
const [profileSuccess, setProfileSuccess] = useState(null);
@@ -55,9 +56,12 @@ export default function ProfilePage() {
useEffect(() => {
setProfileForm({
fullName: user?.fullName || "",
firstName: user?.firstName || "",
lastName: user?.lastName || "",
email: user?.email || "",
phone: user?.phone || "",
password: "",
confirmPassword: "",
});
}, [user]);
@@ -117,11 +121,37 @@ export default function ProfilePage() {
}, [clearPetImageObjectUrls]);
useEffect(() => {
if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
loadPets();
}
}, [user, loadPets]);
useEffect(() => {
let objectUrl = null;
if (user?.avatarUrl && token) {
fetch(`${API_BASE}${user.avatarUrl}`, {
headers: { Authorization: `Bearer ${token}` },
})
.then((res) => (res.ok ? res.blob() : null))
.then((blob) => {
if (blob) {
objectUrl = URL.createObjectURL(blob);
setAvatarObjectUrl(objectUrl);
} else {
setAvatarObjectUrl(null);
}
})
.catch(() => setAvatarObjectUrl(null));
} else {
setAvatarObjectUrl(null);
}
return () => {
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [user?.avatarUrl, token]);
function handleLogout() {
logout();
router.push("/");
@@ -129,10 +159,26 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
async function handleProfileSubmit(e) {
e.preventDefault();
setProfileSubmitting(true);
setProfileError(null);
if (profileForm.password && profileForm.password !== profileForm.confirmPassword) {
setProfileError("Passwords do not match.");
return;
}
setProfileSubmitting(true);
setProfileSuccess(null);
const payload = {
firstName: profileForm.firstName,
lastName: profileForm.lastName,
email: profileForm.email,
phone: profileForm.phone,
};
if (profileForm.password) {
payload.password = profileForm.password;
}
try {
const res = await fetch(`${API_BASE}/api/v1/auth/me`, {
method: "PUT",
@@ -140,7 +186,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(profileForm),
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => null);
@@ -149,6 +195,7 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
}
await refreshUser();
setProfileForm((prev) => ({ ...prev, password: "", confirmPassword: "" }));
setProfileSuccess("Profile updated successfully.");
}
@@ -340,12 +387,14 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
return <main className="auth-page"><p className="profile-loading">Loading</p></main>;
}
const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.username;
const fields = [
{label: "Full Name", value: user.fullName},
{label: "First Name", value: user.firstName || "N/A"},
{label: "Last Name", value: user.lastName || "N/A"},
{label: "Username", value: user.username},
{label: "Email", value: user.email},
{label: "Phone", value: user.phone || ""},
{label: "Role", value: user.role},
{label: "Phone", value: user.phone || "N/A"},
...(user.storeName ? [{ label: "Store", value: user.storeName }] : []),
];
@@ -353,14 +402,14 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
<main className="profile-page-layout">
<div className="profile-card">
<div className="profile-avatar-circle">
{user.avatarUrl ? (
<img src={user.avatarUrl} alt={user.fullName || user.username} className="profile-avatar-image" />
{avatarObjectUrl ? (
<img src={avatarObjectUrl} alt={displayName} className="profile-avatar-image" />
) : (
(user.fullName || user.username).charAt(0).toUpperCase()
displayName.charAt(0).toUpperCase()
)}
</div>
<h1 className="profile-name">{user.fullName || user.username}</h1>
<h1 className="profile-name">{displayName}</h1>
<span className="profile-role-badge">{user.role}</span>
<dl className="profile-fields">
@@ -377,13 +426,23 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
{profileError && <div className="appt-error">{profileError}</div>}
{profileSuccess && <div className="appt-success">{profileSuccess}</div>}
<label className="appt-label">
Full Name
First Name
<input
className="appt-input"
type="text"
value={profileForm.fullName}
onChange={(e) => setProfileForm((current) => ({ ...current, fullName: e.target.value }))}
maxLength={100}
value={profileForm.firstName}
onChange={(e) => setProfileForm((current) => ({ ...current, firstName: e.target.value }))}
maxLength={50}
/>
</label>
<label className="appt-label">
Last Name
<input
className="appt-input"
type="text"
value={profileForm.lastName}
onChange={(e) => setProfileForm((current) => ({ ...current, lastName: e.target.value }))}
maxLength={50}
/>
</label>
<label className="appt-label">
@@ -406,6 +465,29 @@ if (user?.role === "CUSTOMER" || user?.role === "ADMIN") {
maxLength={20}
/>
</label>
<label className="appt-label">
New Password
<input
className="appt-input"
type="password"
value={profileForm.password}
onChange={(e) => setProfileForm((current) => ({ ...current, password: e.target.value }))}
minLength={6}
autoComplete="new-password"
placeholder="Leave blank to keep current"
/>
</label>
<label className="appt-label">
Confirm New Password
<input
className="appt-input"
type="password"
value={profileForm.confirmPassword}
onChange={(e) => setProfileForm((current) => ({ ...current, confirmPassword: e.target.value }))}
autoComplete="new-password"
placeholder="Leave blank to keep current"
/>
</label>
<div className="profile-avatar-actions">
<label className="profile-avatar-upload-btn">
<input

View File

@@ -22,12 +22,14 @@ function RegisterPage() {
const searchParams = useSearchParams();
const [form, setForm] = useState({
fullName: "",
firstName: "",
lastName: "",
username: "",
email: "",
phone: "",
password: "",
confirmPassword: "",});
confirmPassword: "",
});
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
@@ -47,7 +49,9 @@ function RegisterPage() {
setLoading(true);
try {
await register({fullName: form.fullName,
await register({
firstName: form.firstName,
lastName: form.lastName,
username: form.username,
email: form.email,
phone: form.phone,
@@ -74,12 +78,24 @@ function RegisterPage() {
<form className="auth-form" onSubmit={handleSubmit}>
<label className="auth-label">
Full Name
First Name
<input
className="auth-input"
type="text"
name="fullName"
value={form.fullName}
name="firstName"
value={form.firstName}
onChange={handleChange}
required
/>
</label>
<label className="auth-label">
Last Name
<input
className="auth-input"
type="text"
name="lastName"
value={form.lastName}
onChange={handleChange}
required
/>

View File

@@ -79,15 +79,21 @@ export function AuthProvider({ children }) {
return userInfo;
}, [refreshUser]);
const register = useCallback(async ({ username, password, email, fullName, phone }) => {
const register = useCallback(async ({ username, password, email, firstName, lastName, phone }) => {
const res = await fetch("/api/v1/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password, email, fullName, phone }),
body: JSON.stringify({ username, password, email, firstName, lastName, phone }),
});
const data = await res.json();
if (!res.ok) {
if (data.errors && typeof data.errors === "object") {
const fieldErrors = Object.entries(data.errors)
.map(([field, msg]) => `${field}: ${msg}`)
.join(", ");
throw new Error(fieldErrors || data.message || "Registration failed");
}
throw new Error(data.message || "Registration failed");
}