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

@@ -70,7 +70,8 @@ public class AuthController {
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
String username = trimToNull(request.getUsername());
String email = trimToNull(request.getEmail());
NameParts nameParts = splitFullName(request.getFullName());
String firstName = trimToNull(request.getFirstName());
String lastName = trimToNull(request.getLastName());
String phone = normalizePhone(request.getPhone());
if (userRepository.findByUsername(username).isPresent()) {
@@ -98,9 +99,9 @@ public class AuthController {
user.setUsername(username);
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(email);
user.setFirstName(nameParts.firstName());
user.setLastName(nameParts.lastName());
user.setFullName(nameParts.fullName());
user.setFirstName(firstName);
user.setLastName(lastName);
user.setFullName(joinFullName(firstName, lastName));
user.setPhone(phone);
user.setRole(User.Role.CUSTOMER);
user.setActive(true);
@@ -203,11 +204,16 @@ public class AuthController {
user.setEmail(email);
}
if (request.getFullName() != null) {
NameParts nameParts = splitFullName(request.getFullName());
user.setFirstName(nameParts.firstName());
user.setLastName(nameParts.lastName());
user.setFullName(nameParts.fullName());
String firstName = trimToNull(request.getFirstName());
if (firstName != null) {
user.setFirstName(firstName);
}
String lastName = trimToNull(request.getLastName());
if (lastName != null) {
user.setLastName(lastName);
}
if (firstName != null || lastName != null) {
user.setFullName(joinFullName(user.getFirstName(), user.getLastName()));
}
if (request.getPhone() != null) {
@@ -247,6 +253,8 @@ public class AuthController {
return new UserInfoResponse(
user.getId(),
user.getUsername(),
user.getFirstName(),
user.getLastName(),
user.getEmail(),
fullName,
user.getPhone(),

View File

@@ -11,8 +11,11 @@ public class ProfileUpdateRequest {
@Email(message = "Email must be valid")
private String email;
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
@Size(max = 50, message = "First name must not exceed 50 characters")
private String firstName;
@Size(max = 50, message = "Last name must not exceed 50 characters")
private String lastName;
@Size(max = 20, message = "Phone must not exceed 20 characters")
private String phone;
@@ -36,12 +39,20 @@ public class ProfileUpdateRequest {
this.email = email;
}
public String getFullName() {
return fullName;
public String getFirstName() {
return firstName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhone() {
@@ -67,14 +78,15 @@ public class ProfileUpdateRequest {
ProfileUpdateRequest that = (ProfileUpdateRequest) o;
return Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(firstName, that.firstName) &&
Objects.equals(lastName, that.lastName) &&
Objects.equals(phone, that.phone) &&
Objects.equals(password, that.password);
}
@Override
public int hashCode() {
return Objects.hash(username, email, fullName, phone, password);
return Objects.hash(username, email, firstName, lastName, phone, password);
}
@Override
@@ -82,7 +94,8 @@ public class ProfileUpdateRequest {
return "ProfileUpdateRequest{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", phone='" + phone + '\'' +
", password='" + password + '\'' +
'}';

View File

@@ -18,9 +18,13 @@ public class RegisterRequest {
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Full name is required")
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
@NotBlank(message = "First name is required")
@Size(max = 50, message = "First name must not exceed 50 characters")
private String firstName;
@NotBlank(message = "Last name is required")
@Size(max = 50, message = "Last name must not exceed 50 characters")
private String lastName;
@NotBlank(message = "Phone is required")
@Size(max = 20, message = "Phone must not exceed 20 characters")
@@ -50,12 +54,20 @@ public class RegisterRequest {
this.email = email;
}
public String getFullName() {
return fullName;
public String getFirstName() {
return firstName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhone() {
@@ -74,13 +86,14 @@ public class RegisterRequest {
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(firstName, that.firstName) &&
Objects.equals(lastName, that.lastName) &&
Objects.equals(phone, that.phone);
}
@Override
public int hashCode() {
return Objects.hash(username, password, email, fullName, phone);
return Objects.hash(username, password, email, firstName, lastName, phone);
}
@Override
@@ -89,7 +102,8 @@ public class RegisterRequest {
"username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", phone='" + phone + '\'' +
'}';
}

View File

@@ -5,6 +5,8 @@ import java.util.Objects;
public class UserInfoResponse {
private Long id;
private String username;
private String firstName;
private String lastName;
private String email;
private String fullName;
private String phone;
@@ -17,9 +19,11 @@ public class UserInfoResponse {
public UserInfoResponse() {
}
public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
public UserInfoResponse(Long id, String username, String firstName, String lastName, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
this.id = id;
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.fullName = fullName;
this.phone = phone;
@@ -46,6 +50,22 @@ public class UserInfoResponse {
this.username = username;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}

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");
}