Profile image works, editing profile works, now uses first/last name
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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 + '\'' +
|
||||
'}';
|
||||
|
||||
@@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user