Styling refactor
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user