Styling refactor

This commit is contained in:
augmentedpotato
2026-04-18 16:12:43 -06:00
committed by Harkamal Randhawa
parent 148b587c05
commit 79c42574f6
21 changed files with 829 additions and 4509 deletions

View File

@@ -18,6 +18,13 @@ const SPECIES_BREEDS = {
Other: ["Other"],
};
const labelCls = "flex flex-col gap-[0.35rem] text-[0.9rem] font-semibold text-[#444]";
const inputCls = "px-[0.85rem] py-[0.6rem] border border-[#ddd] rounded-lg text-base outline-none transition-all focus:border-[#e68672] focus:shadow-[0_0_0_3px_rgba(230,134,114,0.2)]";
const selectCls = `custom-select ${inputCls} bg-white cursor-pointer`;
const errorCls = "bg-[#fff0f0] border border-[#f5c6c6] text-[#c0392b] rounded-lg px-4 py-3 text-[0.9rem]";
const successCls = "bg-[#f0fdf4] border border-[#bbf7d0] text-[#16a34a] rounded-lg px-4 py-3 text-[0.9rem]";
const submitBtnCls = "py-3 bg-[#e68672] text-white border-none rounded-lg text-base font-bold cursor-pointer transition-all hover:bg-[#d4705e] active:scale-[0.98] disabled:opacity-60 disabled:cursor-not-allowed";
export default function ProfilePage() {
const {user, token, loading, logout, refreshUser} = useAuth();
const router = useRouter();
@@ -53,7 +60,6 @@ export default function ProfilePage() {
if (!loading && !user) {
router.replace(`/login?next=${encodeURIComponent("/profile")}`);
}
}, [user, loading, router]);
useEffect(() => {
@@ -227,9 +233,7 @@ export default function ProfilePage() {
}
async function handleAvatarUpload(file) {
if (!file) {
return;
}
if (!file) return;
const formData = new FormData();
formData.append("avatar", file);
@@ -343,21 +347,19 @@ export default function ProfilePage() {
closeForm();
loadPets();
}
}
catch (err) {
setPetError(err.message);
}
}
finally {
setSubmitting(false);
}
}
async function handleDeletePet(id) {
if (!confirm("Remove this pet profile?")) {
return;
}
if (!confirm("Remove this pet profile?")) return;
try {
const res = await fetch(`${API_BASE}/api/v1/my-pets/${id}`, {
@@ -394,15 +396,19 @@ export default function ProfilePage() {
}
loadPets();
}
}
catch {
alert("Failed to upload image");
}
}
if (loading || !user) {
return <main className="auth-page"><p className="profile-loading">Loading</p></main>;
return (
<main className="min-h-[calc(100vh-70px)] flex items-center justify-center">
<p className="text-[#666] text-[1.1rem]">Loading</p>
</main>
);
}
const displayName = [user.firstName, user.lastName].filter(Boolean).join(" ") || user.username;
@@ -418,101 +424,66 @@ export default function ProfilePage() {
];
return (
<main className="profile-page-layout">
<div className="profile-card">
<div className="profile-avatar-circle">
<main className="min-h-screen max-w-[1000px] mx-auto px-8 py-10 flex flex-col gap-8">
{/* Profile card */}
<div className="bg-white rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.08)] p-8">
<div className="w-20 h-20 rounded-full bg-[#e68672] text-white text-3xl font-bold flex items-center justify-center mx-auto mb-4 overflow-hidden">
{avatarObjectUrl ? (
<img src={avatarObjectUrl} alt={displayName} className="profile-avatar-image" />
<img src={avatarObjectUrl} alt={displayName} className="w-full h-full object-cover" />
) : (
displayName.charAt(0).toUpperCase()
)}
</div>
<h1 className="profile-name">{displayName}</h1>
<span className="profile-role-badge">{user.role}</span>
<h1 className="text-2xl font-bold text-[#222] text-center mb-1">{displayName}</h1>
<div className="flex justify-center mb-6">
<span className="bg-[#f0f0f0] text-[#555] text-[0.75rem] font-semibold rounded-full px-3 py-1">{user.role}</span>
</div>
<dl className="profile-fields">
<dl className="grid grid-cols-2 gap-x-6 gap-y-2 mb-6 bg-[#f9f9f9] rounded-xl p-4 max-[480px]:grid-cols-1">
{fields.map(({ label, value }) => (
<div key={label} className="profile-field-row">
<dt className="profile-field-label">{label}</dt>
<dd className="profile-field-value">{value}</dd>
<div key={label} className="flex gap-2 py-1">
<dt className="text-[0.85rem] font-semibold text-[#888] min-w-[100px]">{label}</dt>
<dd className="text-[0.9rem] text-[#333] m-0">{value}</dd>
</div>
))}
</dl>
<form className="profile-update-form" onSubmit={handleProfileSubmit}>
<h2 className="profile-update-title">Update Profile</h2>
{profileError && <div className="appt-error">{profileError}</div>}
{profileSuccess && <div className="appt-success">{profileSuccess}</div>}
<label className="appt-label">
<form className="flex flex-col gap-4" onSubmit={handleProfileSubmit}>
<h2 className="text-[1.1rem] font-bold text-[#333] mb-0">Update Profile</h2>
{profileError && <div className={errorCls}>{profileError}</div>}
{profileSuccess && <div className={successCls}>{profileSuccess}</div>}
<label className={labelCls}>
First Name
<input
className="appt-input"
type="text"
value={profileForm.firstName}
onChange={(e) => setProfileForm((current) => ({ ...current, firstName: e.target.value }))}
maxLength={50}
/>
<input className={inputCls} type="text" value={profileForm.firstName} onChange={(e) => setProfileForm((c) => ({ ...c, firstName: e.target.value }))} maxLength={50} />
</label>
<label className="appt-label">
<label className={labelCls}>
Last Name
<input
className="appt-input"
type="text"
value={profileForm.lastName}
onChange={(e) => setProfileForm((current) => ({ ...current, lastName: e.target.value }))}
maxLength={50}
/>
<input className={inputCls} type="text" value={profileForm.lastName} onChange={(e) => setProfileForm((c) => ({ ...c, lastName: e.target.value }))} maxLength={50} />
</label>
<label className="appt-label">
<label className={labelCls}>
Email
<input
className="appt-input"
type="email"
value={profileForm.email}
onChange={(e) => setProfileForm((current) => ({ ...current, email: e.target.value }))}
required
/>
<input className={inputCls} type="email" value={profileForm.email} onChange={(e) => setProfileForm((c) => ({ ...c, email: e.target.value }))} required />
</label>
<label className="appt-label">
<label className={labelCls}>
Phone
<input
className="appt-input"
type="text"
value={profileForm.phone}
onChange={(e) => setProfileForm((current) => ({ ...current, phone: e.target.value }))}
maxLength={20}
/>
<input className={inputCls} type="text" value={profileForm.phone} onChange={(e) => setProfileForm((c) => ({ ...c, phone: e.target.value }))} maxLength={20} />
</label>
<label className="appt-label">
<label className={labelCls}>
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"
/>
<input className={inputCls} type="password" value={profileForm.password} onChange={(e) => setProfileForm((c) => ({ ...c, password: e.target.value }))} minLength={6} autoComplete="new-password" placeholder="Leave blank to keep current" />
</label>
<label className="appt-label">
<label className={labelCls}>
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"
/>
<input className={inputCls} type="password" value={profileForm.confirmPassword} onChange={(e) => setProfileForm((c) => ({ ...c, 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">
<div className="flex gap-3 items-center flex-wrap">
<label className="cursor-pointer px-4 py-2 bg-[#e68672] text-white rounded-lg text-[0.85rem] font-semibold hover:bg-[#d4705e] transition-colors">
<input
type="file"
accept="image/jpeg,image/png,image/gif"
className="profile-pet-upload-input"
className="hidden"
onChange={(e) => {
handleAvatarUpload(e.target.files?.[0] || null);
e.target.value = "";
@@ -521,92 +492,70 @@ export default function ProfilePage() {
{avatarSubmitting ? "Working..." : user.avatarUrl ? "Change Avatar" : "Upload Avatar"}
</label>
{user.avatarUrl && (
<button type="button" className="profile-pet-delete-btn" onClick={handleAvatarDelete} disabled={avatarSubmitting}>
<button type="button" className="px-4 py-2 border border-[#f5c6c6] rounded-lg bg-[#fff0f0] text-[#c0392b] text-[0.85rem] cursor-pointer hover:bg-[#ffd7d7] transition-colors disabled:opacity-50" onClick={handleAvatarDelete} disabled={avatarSubmitting}>
Remove Avatar
</button>
)}
</div>
<button type="submit" className="appt-submit-btn" disabled={profileSubmitting || avatarSubmitting}>
<button type="submit" className={submitBtnCls} disabled={profileSubmitting || avatarSubmitting}>
{profileSubmitting ? "Saving..." : "Save Profile"}
</button>
</form>
<button type="button" className="auth-submit-btn profile-logout-btn" onClick={handleLogout}>
<button type="button" className="mt-4 w-full py-3 bg-[#555] text-white border-none rounded-lg text-base font-bold cursor-pointer transition-all hover:bg-[#333] active:scale-[0.98]" onClick={handleLogout}>
Log Out
</button>
</div>
{(user.role === "CUSTOMER" || user.role === "ADMIN") && (
<div className="profile-pets-section">
<div className="profile-pets-header">
<h2 className="profile-pets-title">My Pets</h2>
<button type="button" className="profile-pets-add-btn" onClick={openAddForm}>+ Add Pet</button>
{(user.role === "CUSTOMER" || user.role === "ADMIN") && (
<div className="bg-white rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.08)] p-8">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-[#333] m-0">My Pets</h2>
<button type="button" className="px-4 py-2 bg-[#e68672] text-white rounded-lg text-[0.9rem] font-semibold border-none cursor-pointer hover:bg-[#d4705e] transition-colors" onClick={openAddForm}>+ Add Pet</button>
</div>
{showForm && (
<form className="profile-pet-form" onSubmit={handlePetSubmit}>
<h3 className="profile-pet-form-title">
<form className="bg-[#f9f9f9] rounded-xl p-6 mb-6 flex flex-col gap-4" onSubmit={handlePetSubmit}>
<h3 className="text-[1.1rem] font-bold text-[#333] m-0">
{editingPet ? "Edit Pet" : "Add a New Pet"}
</h3>
{petError && <div className="appt-error">{petError}</div>}
<label className="appt-label">
{petError && <div className={errorCls}>{petError}</div>}
<label className={labelCls}>
Name
<input
className="appt-input"
type="text"
value={petName}
onChange={(e) => setPetName(e.target.value)}
required
maxLength={50}
/>
<input className={inputCls} type="text" value={petName} onChange={(e) => setPetName(e.target.value)} required maxLength={50} />
</label>
<label className="appt-label">
<label className={labelCls}>
Species
<select
className="appt-select"
value={species}
onChange={(e) => { setSpecies(e.target.value); setBreed(""); }}
required
>
<select className={selectCls} value={species} onChange={(e) => { setSpecies(e.target.value); setBreed(""); }} required>
<option value="">Select a species...</option>
{Object.keys(SPECIES_BREEDS).map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</label>
<label className="appt-label">
<label className={labelCls}>
Breed
<select
className="appt-select"
value={breed}
onChange={(e) => setBreed(e.target.value)}
required
disabled={!species}
>
<select className={`${selectCls} disabled:bg-[#f5f5f5] disabled:text-[#aaa] disabled:cursor-not-allowed`} value={breed} onChange={(e) => setBreed(e.target.value)} required disabled={!species}>
<option value="">{species ? "Select a breed..." : "Select a species first"}</option>
{(SPECIES_BREEDS[species] || []).map((b) => (
<option key={b} value={b}>{b}</option>
))}
</select>
</label>
<label className="appt-label">
<label className={labelCls}>
Age (years)
<select
className="appt-select"
value={petAge}
onChange={(e) => setPetAge(e.target.value)}
required
>
<select className={selectCls} value={petAge} onChange={(e) => setPetAge(e.target.value)} required>
{Array.from({ length: 20 }, (_, i) => i + 1).map((n) => (
<option key={n} value={n}>{n}</option>
))}
</select>
</label>
<div className="profile-pet-form-actions">
<button type="submit" className="appt-submit-btn" disabled={submitting}>
<div className="flex gap-3">
<button type="submit" className={submitBtnCls} disabled={submitting}>
{submitting ? "Saving..." : editingPet ? "Save Changes" : "Add Pet"}
</button>
<button type="button" className="profile-pet-cancel-btn" onClick={closeForm}>
<button type="button" className="px-4 py-2 border border-[#ddd] rounded-lg bg-white text-[#555] text-[0.9rem] cursor-pointer hover:border-[#aaa] transition-colors" onClick={closeForm}>
Cancel
</button>
</div>
@@ -614,28 +563,24 @@ export default function ProfilePage() {
)}
{loadingPets ? (
<p className="appt-loading">Loading pets...</p>
<p className="text-[#666] text-center py-4">Loading pets...</p>
) : pets.length === 0 && !showForm ? (
<p className="profile-pets-empty">No pet profiles yet. Add your first pet above!</p>
<p className="text-center text-[#888] py-8">No pet profiles yet. Add your first pet above!</p>
) : (
<div className="profile-pets-grid">
<div className="grid grid-cols-3 gap-4 max-[768px]:grid-cols-2 max-[480px]:grid-cols-1">
{pets.map((pet) => (
<div key={pet.customerPetId} className="profile-pet-card">
<div className="profile-pet-card-img-area">
<div key={pet.customerPetId} className="bg-[#f9f9f9] rounded-xl overflow-hidden border border-[#eee]">
<div className="relative aspect-square overflow-hidden bg-[#ececec]">
{pet.imageUrl ? (
<img
src={pet.imageUrl}
alt={pet.petName}
className="profile-pet-card-img"
/>
<img src={pet.imageUrl} alt={pet.petName} className="w-full h-full object-cover" />
) : (
<div className="profile-pet-card-placeholder">🐾</div>
<div className="w-full h-full flex items-center justify-center text-4xl">🐾</div>
)}
<label className="profile-pet-upload-label">
<label className="absolute bottom-2 right-2 bg-white rounded-full w-8 h-8 flex items-center justify-center cursor-pointer shadow text-base">
<input
type="file"
accept="image/jpeg,image/png,image/gif"
className="profile-pet-upload-input"
className="hidden"
onChange={(e) => {
if (e.target.files[0]) handleImageUpload(pet.customerPetId, e.target.files[0]);
e.target.value = "";
@@ -644,15 +589,15 @@ export default function ProfilePage() {
📷
</label>
</div>
<div className="profile-pet-card-info">
<span className="profile-pet-card-name">{pet.petName}</span>
<span className="profile-pet-card-detail">{pet.species}</span>
{pet.breed && <span className="profile-pet-card-detail">{pet.breed}</span>}
{pet.petAge != null && <span className="profile-pet-card-detail">Age: {pet.petAge === 0 ? "< 1 yr" : `${pet.petAge} yr${pet.petAge !== 1 ? "s" : ""}`}</span>}
<div className="p-3 flex flex-col gap-1">
<span className="font-semibold text-[#222] text-[0.95rem]">{pet.petName}</span>
<span className="text-[0.82rem] text-[#666]">{pet.species}</span>
{pet.breed && <span className="text-[0.82rem] text-[#666]">{pet.breed}</span>}
{pet.petAge != null && <span className="text-[0.82rem] text-[#666]">Age: {pet.petAge === 0 ? "< 1 yr" : `${pet.petAge} yr${pet.petAge !== 1 ? "s" : ""}`}</span>}
</div>
<div className="profile-pet-card-actions">
<button type="button" className="profile-pet-edit-btn" onClick={() => openEditForm(pet)}>Edit</button>
<button type="button" className="profile-pet-delete-btn" onClick={() => handleDeletePet(pet.customerPetId)}>Remove</button>
<div className="flex gap-2 px-3 pb-3">
<button type="button" className="px-3 py-1.5 border border-[#ddd] rounded-lg bg-white text-[#333] text-[0.82rem] cursor-pointer hover:border-[#e68672] transition-colors" onClick={() => openEditForm(pet)}>Edit</button>
<button type="button" className="px-3 py-1.5 border border-[#f5c6c6] rounded-lg bg-[#fff0f0] text-[#c0392b] text-[0.82rem] cursor-pointer hover:bg-[#ffd7d7] transition-colors" onClick={() => handleDeletePet(pet.customerPetId)}>Remove</button>
</div>
</div>
))}
@@ -662,34 +607,34 @@ export default function ProfilePage() {
)}
{(user.role === "CUSTOMER" || user.role === "ADMIN") && (
<div className="profile-pets-section">
<div className="profile-pets-header">
<h2 className="profile-pets-title">Order History</h2>
<div className="bg-white rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.08)] p-8">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-[#333] m-0">Order History</h2>
</div>
{loadingOrders ? (
<p className="appt-loading">Loading orders...</p>
<p className="text-[#666] text-center py-4">Loading orders...</p>
) : orders.length === 0 ? (
<p className="profile-pets-empty">No orders yet.</p>
<p className="text-center text-[#888] py-8">No orders yet.</p>
) : (
<div className="profile-orders-list">
<div className="flex flex-col gap-4">
{orders.map((order) => (
<div key={order.saleId} className="profile-order-card">
<div className="profile-order-header">
<span className="profile-order-date">
<div key={order.saleId} className="bg-[#f9f9f9] rounded-xl p-4 border border-[#eee]">
<div className="flex items-center justify-between mb-2">
<span className="text-[0.9rem] font-semibold text-[#444]">
{new Date(order.saleDate).toLocaleDateString([], { year: "numeric", month: "short", day: "numeric" })}
</span>
<span className="profile-order-total">${Number(order.totalAmount).toFixed(2)}</span>
<span className="text-[1rem] font-bold text-[#e68672]">${Number(order.totalAmount).toFixed(2)}</span>
</div>
<div className="profile-order-meta">
<div className="flex gap-4 text-[0.85rem] text-[#888] mb-2">
<span>{order.storeName}</span>
{order.paymentMethod && <span>{order.paymentMethod}</span>}
</div>
{order.items?.length > 0 && (
<ul className="profile-order-items">
<ul className="list-none m-0 p-0 flex flex-col gap-1 border-t border-[#eee] pt-2 mt-2">
{order.items.map((item) => (
<li key={item.saleItemId}>
<li key={item.saleItemId} className="flex justify-between text-[0.85rem] text-[#555]">
<span>{item.productName} × {item.quantity}</span>
<span className="profile-order-item-price">${(Number(item.unitPrice) * item.quantity).toFixed(2)}</span>
<span className="font-semibold">${(Number(item.unitPrice) * item.quantity).toFixed(2)}</span>
</li>
))}
</ul>