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

@@ -39,8 +39,7 @@ function getAvailableServices(services, species) {
}
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December",
];
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
function DatePicker({ value, minDate, onChange }) {
const today = new Date();
@@ -53,24 +52,13 @@ function DatePicker({ value, minDate, onChange }) {
const [viewMonth, setViewMonth] = useState(parsed ? parsed.getMonth() : min.getMonth());
function prevMonth() {
if (viewMonth === 0) {
setViewMonth(11);
setViewYear((y) => y - 1);
}
else {
setViewMonth((m) => m - 1);
}
if (viewMonth === 0) { setViewMonth(11); setViewYear((y) => y - 1); }
else { setViewMonth((m) => m - 1); }
}
function nextMonth() {
if (viewMonth === 11) {
setViewMonth(0); setViewYear((y) => y + 1);
}
else {
setViewMonth((m) => m + 1);
}
if (viewMonth === 11) { setViewMonth(0); setViewYear((y) => y + 1); }
else { setViewMonth((m) => m + 1); }
}
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
@@ -82,11 +70,8 @@ function DatePicker({ value, minDate, onChange }) {
function selectDay(day) {
const d = new Date(viewYear, viewMonth, day);
if (d < min) {
return;
}
if (d < min) return;
const iso = `${viewYear}-${String(viewMonth + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
onChange(iso);
}
@@ -107,94 +92,16 @@ function DatePicker({ value, minDate, onChange }) {
cells.push({ key: `day-${viewYear}-${viewMonth}-${String(d)}`, day: d });
}
const s = {
widget: {
border: "1px solid #ddd",
borderRadius: "10px",
overflow: "hidden",
background: "white",
userSelect: "none",
fontFamily: "inherit",
},
header: {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "orange",
padding: "0.55rem 0.75rem",
},
monthLabel: {
fontSize: "0.95rem",
fontWeight: 700,
color: "white",
},
nav: {
background: "none",
border: "none",
color: "white",
fontSize: "1.5rem",
lineHeight: 1,
cursor: "pointer",
padding: "0 0.4rem",
borderRadius: "4px",
},
grid: {
display: "grid",
gridTemplateColumns: "repeat(7, 1fr)",
gap: "3px",
padding: "0.6rem",
},
dayName: {
textAlign: "center",
fontSize: "0.7rem",
fontWeight: 700,
color: "#aaa",
padding: "0.25rem 0",
textTransform: "uppercase",
},
dayBase: {
display: "flex",
alignItems: "center",
justifyContent: "center",
aspectRatio: "1 / 1",
border: "none",
borderRadius: "6px",
background: "none",
fontSize: "0.875rem",
cursor: "pointer",
color: "#333",
fontFamily: "inherit",
padding: 0,
width: "100%",
},
daySelected: {
background: "orange",
color: "white",
fontWeight: 700,
},
dayDisabled: {
color: "#ccc",
cursor: "default",
},
selectedLabel: {
textAlign: "center",
fontSize: "0.82rem",
color: "#666",
padding: "0.35rem 0.5rem 0.5rem",
borderTop: "1px solid #f0f0f0",
},
};
return (
<div style={s.widget}>
<div style={s.header}>
<button type="button" style={s.nav} onClick={prevMonth} disabled={isPrevDisabled} aria-label="Previous month"></button>
<span style={s.monthLabel}>{MONTHS[viewMonth]} {viewYear}</span>
<button type="button" style={s.nav} onClick={nextMonth} aria-label="Next month"></button>
<div className="border border-[#ddd] rounded-[10px] overflow-hidden bg-white select-none">
<div className="flex items-center justify-between bg-orange-500 px-3 py-[0.55rem]">
<button type="button" className="bg-transparent border-none text-white text-2xl leading-none cursor-pointer px-[0.4rem] rounded hover:bg-white/20 disabled:opacity-40" onClick={prevMonth} disabled={isPrevDisabled} aria-label="Previous month"></button>
<span className="text-[0.95rem] font-bold text-white">{MONTHS[viewMonth]} {viewYear}</span>
<button type="button" className="bg-transparent border-none text-white text-2xl leading-none cursor-pointer px-[0.4rem] rounded hover:bg-white/20" onClick={nextMonth} aria-label="Next month"></button>
</div>
<div style={s.grid}>
<div className="grid grid-cols-7 gap-[3px] p-[0.6rem]">
{DAYS.map((d) => (
<span key={d} style={s.dayName}>{d}</span>
<span key={d} className="text-center text-[0.7rem] font-bold text-[#aaa] py-1 uppercase">{d}</span>
))}
{cells.map(({ key, day }) =>
day === null ? (
@@ -203,11 +110,8 @@ function DatePicker({ value, minDate, onChange }) {
<button
key={key}
type="button"
style={{
...s.dayBase,
...(isSelected(day) ? s.daySelected : {}),
...(isDisabled(day) ? s.dayDisabled : {}),
}}
className={`flex items-center justify-center aspect-square border-none rounded-md text-[0.875rem] cursor-pointer w-full p-0 transition-colors
${isSelected(day) ? "bg-orange-500 text-white font-bold" : isDisabled(day) ? "text-[#ccc] cursor-default bg-transparent" : "bg-transparent text-[#333] hover:bg-orange-100"}`}
onClick={() => selectDay(day)}
disabled={isDisabled(day)}
aria-pressed={isSelected(day)}
@@ -218,7 +122,7 @@ function DatePicker({ value, minDate, onChange }) {
)}
</div>
{parsed && (
<div style={s.selectedLabel}>
<div className="text-center text-[0.82rem] text-[#666] px-2 pb-2 pt-1 border-t border-[#f0f0f0]">
Selected: {MONTHS[parsed.getMonth()]} {parsed.getDate()}, {parsed.getFullYear()}
</div>
)}
@@ -226,6 +130,13 @@ function DatePicker({ value, minDate, onChange }) {
);
}
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";
function AddPetModal({ token, onClose, onAdded }) {
const [petName, setPetName] = useState("");
const [species, setSpecies] = useState("");
@@ -267,56 +178,38 @@ function AddPetModal({ token, onClose, onAdded }) {
}
return (
<div className="appt-modal-overlay" onClick={onClose}>
<div className="appt-modal" onClick={(e) => e.stopPropagation()}>
<h3 className="profile-pet-form-title">Add a New Pet</h3>
{petError && <div className="appt-error">{petError}</div>}
<form onSubmit={handleSubmit}>
<label className="appt-label">
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4" onClick={onClose}>
<div className="bg-white rounded-2xl shadow-xl p-8 w-full max-w-[480px] flex flex-col gap-4" onClick={(e) => e.stopPropagation()}>
<h3 className="text-[1.1rem] font-bold text-[#333] m-0">Add a New Pet</h3>
{petError && <div className={errorCls}>{petError}</div>}
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
<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>
<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..." : "Add Pet"}
</button>
<button type="button" className="profile-pet-cancel-btn" onClick={onClose}>
<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={onClose}>
Cancel
</button>
</div>
@@ -332,7 +225,6 @@ function AppointmentsPage() {
const searchParams = useSearchParams();
const preselectedPetId = searchParams.get("petId");
// Adoption mode — set when arriving from a pet detail page
const adoptionMode = searchParams.get("adoptionMode") === "true";
const adoptionPetId = searchParams.get("petId");
const adoptionPetName = searchParams.get("petName") || "";
@@ -345,7 +237,6 @@ function AppointmentsPage() {
const errorRef = useRef(null);
const historyRef = useRef(null);
// Adoption-mode URL verification
const [adoptionVerified, setAdoptionVerified] = useState(!adoptionMode);
const [adoptionVerifyError, setAdoptionVerifyError] = useState(null);
const [adoptionVerifyLoading, setAdoptionVerifyLoading] = useState(adoptionMode);
@@ -388,17 +279,15 @@ function AppointmentsPage() {
const [showPastAppts, setShowPastAppts] = useState(false);
const [showPastAdoptions, setShowPastAdoptions] = useState(false);
const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
useEffect(() => {
if (!authLoading && !user) {
const target = preselectedPetId ? `/appointments?petId=${encodeURIComponent(preselectedPetId)}` : "/appointments";
router.push(`/login?next=${encodeURIComponent(target)}`);
}
}, [authLoading, user, router, preselectedPetId]);
// Verify the pet from the URL is real, available, and at the stated store
useEffect(() => {
if (!adoptionMode || !adoptionPetId) return;
setAdoptionVerifyLoading(true);
@@ -431,9 +320,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
}, [token, canBookAppointments]);
useEffect(() => {
if (!token) {
return;
}
if (!token) return;
fetch(`${API_BASE}/api/v1/dropdowns/stores`, {
headers: { Authorization: `Bearer ${token}` },
@@ -459,10 +346,8 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
if (didPreselectRef.current) return;
if (adoptionMode) {
// Need both the store (so employees load) and a serviceId (so availability slots load)
if (adoptionStoreId && services.length > 0) {
setStoreId(adoptionStoreId);
// Prefer a service named "adopt", fall back to the first available service
const adoptionSvc =
services.find((s) => s.serviceName.toLowerCase().includes("adopt")) ||
services[0];
@@ -474,14 +359,9 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
return;
}
if (!preselectedPetId || services.length === 0 || allPets.length === 0) {
return;
}
const adoptionSvc = services.find((s) =>
s.serviceName.toLowerCase().includes("adopt")
);
if (!preselectedPetId || services.length === 0 || allPets.length === 0) return;
const adoptionSvc = services.find((s) => s.serviceName.toLowerCase().includes("adopt"));
if (adoptionSvc) {
setServiceId(String(adoptionSvc.serviceId));
}
@@ -574,11 +454,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
}, [token, storeId]);
useEffect(() => {
if (!employees.length) {
setEmployeeId("");
return;
}
if (!employees.length) { setEmployeeId(""); return; }
const currentExists = employees.some((employee) => String(employee.id) === String(employeeId));
if (!currentExists) {
setEmployeeId(String(employees[0].id));
@@ -589,7 +465,6 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
if (!storeId || !serviceId || !appointmentDate) {
setAvailableSlots([]);
setAppointmentTime("");
return;
}
setLoadingSlots(true);
@@ -597,10 +472,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
const params = new URLSearchParams({ storeId, serviceId, date: appointmentDate });
fetch(`${API_BASE}/api/v1/appointments/availability?${params}`)
.then((r) => {
if (!r.ok) {
throw new Error("Failed to check availability");
}
if (!r.ok) throw new Error("Failed to check availability");
return r.json();
})
.then(setAvailableSlots)
@@ -640,13 +512,11 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
const hour = parseInt(h, 10);
const ampm = hour >= 12 ? "PM" : "AM";
const display = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
return `${display}:${m} ${ampm}`;
}
function getMinDate() {
const d = new Date();
return d.toISOString().split("T")[0];
}
@@ -686,7 +556,6 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
try {
if (adoptionMode) {
// Submit an adoption request directly to the adoption table
const body = {
petId: Number(adoptionPetId),
employeeId: employeeId ? Number(employeeId) : undefined,
@@ -740,7 +609,6 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
if (!res.ok) {
const data = await res.json().catch(() => null);
throw new Error(data?.message || data?.error || `Request failed (${res.status})`);
}
@@ -753,22 +621,21 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
setAvailableSlots([]);
loadAppointments();
setTimeout(() => historyRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }), 300);
}
}
catch (err) {
setError(err.message);
}
}
finally {
setSubmitting(false);
}
}
if (authLoading) {
return (
<main className="appt-page">
<p className="appt-loading">Loading...</p>
<main className="min-h-screen">
<p className="text-center text-[#666] py-12">Loading...</p>
</main>
);
}
@@ -776,7 +643,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
if (!user) return null;
return (
<main className="appt-page">
<main className="min-h-screen">
{showAddPetModal && (
<AddPetModal
token={token}
@@ -785,63 +652,68 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
/>
)}
<section className="appt-hero">
<h1 className="appt-hero-title">{adoptionMode ? "Schedule an Adoption" : "Schedule an Appointment"}</h1>
<p className="appt-hero-subtitle">{adoptionMode ? "Schedule a pet adoption visit" : "Book a service for your pet."}</p>
<div className="title-decoration"></div>
<section className="text-center py-16 px-8 bg-gradient-to-b from-[#f9f9f9] to-white">
<h1 className="text-5xl font-bold text-[#333] mb-4 tracking-tight max-[768px]:text-3xl max-[480px]:text-[1.6rem]">
{adoptionMode ? "Schedule an Adoption" : "Schedule an Appointment"}
</h1>
<p className="text-2xl font-light text-[#666] mb-8 max-[768px]:text-[1.2rem]">
{adoptionMode ? "Schedule a pet adoption visit" : "Book a service for your pet."}
</p>
<div className="w-[100px] h-1 bg-[#e68672] mx-auto mt-8 rounded-sm"></div>
</section>
<section className="appt-content">
<section className="max-w-[1200px] mx-auto px-8 pb-16 flex gap-8 items-start max-[900px]:flex-col">
{canBookAppointments ? (
<form className="appt-form" onSubmit={handleSubmit}>
<h2 className="appt-form-title">{adoptionMode ? "New Adoption" : "New Appointment"}</h2>
<form className="bg-white rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.08)] p-8 flex-1 flex flex-col gap-4 max-[900px]:w-full" onSubmit={handleSubmit}>
<h2 className="text-xl font-bold text-[#333] m-0">{adoptionMode ? "New Adoption" : "New Appointment"}</h2>
{error && <div className="appt-error" ref={errorRef}>{error}</div>}
{error && <div className={errorCls} ref={errorRef}>{error}</div>}
{adoptionMode && adoptionVerifyLoading && (
<p className="appt-loading">Verifying pet details</p>
<p className="text-[#666] text-center py-2">Verifying pet details</p>
)}
{adoptionMode && adoptionVerifyError && (
<div className="appt-error">{adoptionVerifyError}</div>
<div className={errorCls}>{adoptionVerifyError}</div>
)}
{(!adoptionMode || adoptionVerified) && (<>
{/* ADOPTION MODE: locked pet + store */}
{adoptionMode && (
<label className="appt-label">
<label className={labelCls}>
Pet
<div className="appt-locked-field">
<div className="px-[0.85rem] py-[0.6rem] border border-[#ddd] rounded-lg bg-[#f5f5f5] text-[#555]">
{[adoptionPetName, adoptionPetSpecies, adoptionPetBreed].filter(Boolean).join(" · ")}
</div>
</label>
)}
{/* STEP 1 (non-adoption): select a pet first */}
{!adoptionMode && (
<div className="appt-label">
<div className={labelCls}>
<span>Select Your Pet</span>
{eligiblePets.length === 0 ? (
<p className="appt-no-slots">
<p className="text-[#888] text-[0.9rem] m-0">
You have no adopted pets available.{" "}
<Link href="/profile">Add a pet on your profile page.</Link>
<Link href="/profile" className="text-[#e68672] hover:underline">Add a pet on your profile page.</Link>
</p>
) : (
<div className="appt-pets-grid">
<div className="flex flex-wrap gap-2">
{eligiblePets.map((p) => (
<label
key={p.customerPetId}
className={`appt-pet-chip ${selectedPetIds.includes(p.customerPetId) ? "appt-pet-chip--selected" : ""}`}
className={`flex items-center gap-2 cursor-pointer px-3 py-2 rounded-full border-2 text-[0.85rem] font-semibold transition-all
${selectedPetIds.includes(p.customerPetId)
? "border-[#e68672] bg-[#fff4f2] text-[#e68672]"
: "border-[#ddd] bg-white text-[#333] hover:border-[#e68672]"}`}
>
<input
type="radio"
name="customerPet"
checked={selectedPetIds.includes(p.customerPetId)}
onChange={() => handlePetSelect(p.customerPetId)}
className="appt-pet-checkbox"
className="hidden"
/>
{p.petName}
<span className="appt-pet-chip-species">({p.species})</span>
<span className="text-[#888] font-normal">({p.species})</span>
</label>
))}
</div>
@@ -849,20 +721,14 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
</div>
)}
{/* Remaining fields — shown after pet selected (or always in adoption mode) */}
{(adoptionMode || selectedPetIds.length > 0) && (<>
<label className="appt-label">
<label className={labelCls}>
Store Location
{adoptionMode ? (
<div className="appt-locked-field">{adoptionStoreName || "Pet's store"}</div>
<div className="px-[0.85rem] py-[0.6rem] border border-[#ddd] rounded-lg bg-[#f5f5f5] text-[#555]">{adoptionStoreName || "Pet's store"}</div>
) : (
<select
className="appt-select"
value={storeId}
onChange={(e) => setStoreId(e.target.value)}
required
>
<select className={selectCls} value={storeId} onChange={(e) => setStoreId(e.target.value)} required>
<option value="">Select a store...</option>
{stores.map((s) => (
<option key={s.id} value={s.id}>{s.label}</option>
@@ -872,19 +738,14 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
</label>
{!adoptionMode && (
<label className="appt-label">
<label className={labelCls}>
Service
{availableServices.length === 0 ? (
<p className="appt-no-slots">
<p className="text-[#888] text-[0.9rem] m-0">
No services are available for {selectedPet?.species || "this pet"}.
</p>
) : (
<select
className="appt-select"
value={serviceId}
onChange={(e) => handleServiceChange(e.target.value)}
required
>
<select className={selectCls} value={serviceId} onChange={(e) => handleServiceChange(e.target.value)} required>
<option value="">Select a service...</option>
{availableServices.map((s) => (
<option key={s.serviceId} value={s.serviceId}>
@@ -897,13 +758,9 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
)}
{employees.length > 0 && (
<label className="appt-label">
<label className={labelCls}>
Employee
<select
className="appt-select"
value={employeeId}
onChange={(e) => setEmployeeId(e.target.value)}
>
<select className={selectCls} value={employeeId} onChange={(e) => setEmployeeId(e.target.value)}>
{employees.map((employee) => (
<option key={employee.id} value={employee.id}>{employee.label}</option>
))}
@@ -912,12 +769,12 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
)}
{!adoptionMode && selectedService && (
<div className="appt-service-info">
<p>{selectedService.serviceDesc}</p>
<div className="bg-[#f9f9f9] rounded-lg px-4 py-3 text-[0.85rem] text-[#555] border border-[#eee]">
<p className="m-0">{selectedService.serviceDesc}</p>
</div>
)}
<div className="appt-label">
<div className={labelCls}>
Date
<DatePicker
value={appointmentDate}
@@ -927,19 +784,22 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
</div>
{!adoptionMode && storeId && serviceId && appointmentDate && (
<div className="appt-label">
<div className={labelCls}>
<span>Available Time Slots</span>
{loadingSlots ? (
<p className="appt-slots-loading">Checking availability...</p>
<p className="text-[#888] text-[0.9rem] m-0">Checking availability...</p>
) : availableSlots.length === 0 ? (
<p className="appt-no-slots">No available slots for this date. Please try another date.</p>
<p className="text-[#888] text-[0.9rem] m-0">No available slots for this date. Please try another date.</p>
) : (
<div className="appt-slots-grid">
<div className="flex flex-wrap gap-2">
{availableSlots.map((slot) => (
<button
key={slot}
type="button"
className={`appt-slot-btn ${appointmentTime === slot ? "appt-slot-btn--selected" : ""}`}
className={`px-3 py-2 rounded-lg border-2 text-[0.85rem] cursor-pointer transition-all
${appointmentTime === slot
? "border-[#e68672] bg-[#e68672] text-white"
: "border-[#ddd] bg-white text-[#333] hover:border-[#e68672]"}`}
onClick={() => setAppointmentTime(slot)}
>
{formatTime(slot)}
@@ -952,24 +812,21 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
</>)}
<button
type="submit"
className="appt-submit-btn"
disabled={!formValid || submitting}
>
<button type="submit" className={submitBtnCls} disabled={!formValid || submitting}>
{submitting ? "Booking..." : adoptionMode ? "Schedule Adoption" : "Book Appointment"}
</button>
{success && <div className="appt-success">{success}</div>}
{success && <div className={successCls}>{success}</div>}
</>)}
</form>
) : null}
<div className="appt-history" ref={historyRef}>
<h2 className="appt-form-title">{canBookAppointments ? "Your Appointments" : "Appointments"}</h2>
{/* History panel */}
<div className="bg-white rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.08)] p-8 flex-1 flex flex-col gap-4 max-[900px]:w-full" ref={historyRef}>
<h2 className="text-xl font-bold text-[#333] m-0">{canBookAppointments ? "Your Appointments" : "Appointments"}</h2>
{loadingAppointments ? (
<p className="appt-loading">Loading appointments...</p>
<p className="text-[#666] text-center py-4">Loading appointments...</p>
) : (() => {
const activeAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() === "booked");
const pastAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() !== "booked");
@@ -980,35 +837,35 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
return (
<>
<input
className="appt-search"
className="px-4 py-2 border border-[#ddd] rounded-lg text-[0.9rem] outline-none w-full focus:border-[#e68672] transition-colors"
type="text"
placeholder="Search appointments…"
value={apptSearch}
onChange={(e) => setApptSearch(e.target.value)}
/>
{filteredActive.length === 0 ? (
<p className="appt-empty">{activeAppts.length === 0 ? "No active appointments." : "No results."}</p>
<p className="text-[#888] text-[0.9rem] py-4 m-0">{activeAppts.length === 0 ? "No active appointments." : "No results."}</p>
) : (
<div className="appt-list">
<div className="flex flex-col gap-3">
{filteredActive.map((a) => (
<div key={a.appointmentId} className="appt-card">
<div className="appt-card-header">
<span className="appt-card-service">{a.serviceName}</span>
<span className={`appt-card-status appt-card-status--${a.appointmentStatus?.toLowerCase()}`}>
<div key={a.appointmentId} className="bg-[#f9f9f9] rounded-xl p-4 border border-[#eee]">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-[#333]">{a.serviceName}</span>
<span className={`text-xs font-semibold rounded-full px-2.5 py-1 appt-card-status--${a.appointmentStatus?.toLowerCase()}`}>
{a.appointmentStatus}
</span>
</div>
<div className="appt-card-details">
<div className="flex gap-4 text-[0.85rem] text-[#666] mb-2 flex-wrap">
<span>{a.storeName}</span>
<span>{a.appointmentDate} at {formatTime(a.appointmentTime)}</span>
</div>
{a.petName && (
<div className="appt-card-pets">Pet: {a.petName}</div>
<div className="text-[0.85rem] text-[#888] mb-2">Pet: {a.petName}</div>
)}
<div className="appt-card-actions">
<div className="flex gap-2">
<button
type="button"
className="appt-cancel-btn"
className="px-3 py-1.5 rounded-lg border border-[#f5c6c6] bg-[#fff0f0] text-[#c0392b] text-[0.85rem] cursor-pointer hover:bg-[#ffd7d7] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={cancellingId === a.appointmentId}
onClick={() => handleCancelAppointment(a.appointmentId)}
>
@@ -1020,26 +877,26 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
</div>
)}
{pastAppts.length > 0 && (
<div className="appt-past-section">
<button className="appt-past-toggle" onClick={() => setShowPastAppts((v) => !v)}>
<div className="mt-2">
<button className="text-[0.85rem] text-[#e68672] cursor-pointer bg-transparent border-none font-semibold hover:underline" onClick={() => setShowPastAppts((v) => !v)}>
{showPastAppts ? "Hide" : "Show"} past appointments ({pastAppts.length})
</button>
{showPastAppts && (
<div className="appt-list appt-list--past">
<div className="flex flex-col gap-3 mt-3 opacity-75">
{pastAppts.map((a) => (
<div key={a.appointmentId} className="appt-card appt-card--past">
<div className="appt-card-header">
<span className="appt-card-service">{a.serviceName}</span>
<span className={`appt-card-status appt-card-status--${a.appointmentStatus?.toLowerCase()}`}>
<div key={a.appointmentId} className="bg-[#f9f9f9] rounded-xl p-4 border border-[#eee]">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-[#333]">{a.serviceName}</span>
<span className={`text-xs font-semibold rounded-full px-2.5 py-1 appt-card-status--${a.appointmentStatus?.toLowerCase()}`}>
{a.appointmentStatus}
</span>
</div>
<div className="appt-card-details">
<div className="flex gap-4 text-[0.85rem] text-[#666] flex-wrap">
<span>{a.storeName}</span>
<span>{a.appointmentDate} at {formatTime(a.appointmentTime)}</span>
</div>
{a.petName && (
<div className="appt-card-pets">Pet: {a.petName}</div>
<div className="text-[0.85rem] text-[#888] mt-1">Pet: {a.petName}</div>
)}
</div>
))}
@@ -1051,9 +908,9 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
);
})()}
<h2 className="appt-form-title" style={{ marginTop: "2rem" }}>{canBookAppointments ? "Your Adoptions" : "Adoptions"}</h2>
<h2 className="text-xl font-bold text-[#333] m-0 mt-4">{canBookAppointments ? "Your Adoptions" : "Adoptions"}</h2>
{loadingAdoptions ? (
<p className="appt-loading">Loading adoptions...</p>
<p className="text-[#666] text-center py-4">Loading adoptions...</p>
) : (() => {
const activeAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() === "pending");
const pastAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() !== "pending");
@@ -1064,32 +921,32 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
return (
<>
<input
className="appt-search"
className="px-4 py-2 border border-[#ddd] rounded-lg text-[0.9rem] outline-none w-full focus:border-[#e68672] transition-colors"
type="text"
placeholder="Search adoptions…"
value={adoptionSearch}
onChange={(e) => setAdoptionSearch(e.target.value)}
/>
{filteredActive.length === 0 ? (
<p className="appt-empty">{activeAdoptions.length === 0 ? "No active adoption requests." : "No results."}</p>
<p className="text-[#888] text-[0.9rem] py-4 m-0">{activeAdoptions.length === 0 ? "No active adoption requests." : "No results."}</p>
) : (
<div className="appt-list">
<div className="flex flex-col gap-3">
{filteredActive.map((a) => (
<div key={a.adoptionId} className="appt-card">
<div className="appt-card-header">
<span className="appt-card-service">{a.petName}</span>
<span className={`appt-card-status appt-card-status--${a.adoptionStatus?.toLowerCase()}`}>
<div key={a.adoptionId} className="bg-[#f9f9f9] rounded-xl p-4 border border-[#eee]">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-[#333]">{a.petName}</span>
<span className={`text-xs font-semibold rounded-full px-2.5 py-1 appt-card-status--${a.adoptionStatus?.toLowerCase()}`}>
{a.adoptionStatus}
</span>
</div>
<div className="appt-card-details">
<div className="flex gap-4 text-[0.85rem] text-[#666] mb-2 flex-wrap">
<span>{a.sourceStoreName}</span>
<span>{a.adoptionDate}</span>
</div>
<div className="appt-card-actions">
<div className="flex gap-2">
<button
type="button"
className="appt-cancel-btn"
className="px-3 py-1.5 rounded-lg border border-[#f5c6c6] bg-[#fff0f0] text-[#c0392b] text-[0.85rem] cursor-pointer hover:bg-[#ffd7d7] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={cancellingId === a.adoptionId}
onClick={() => handleCancelAdoption(a.adoptionId)}
>
@@ -1101,21 +958,21 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
</div>
)}
{pastAdoptions.length > 0 && (
<div className="appt-past-section">
<button className="appt-past-toggle" onClick={() => setShowPastAdoptions((v) => !v)}>
<div className="mt-2">
<button className="text-[0.85rem] text-[#e68672] cursor-pointer bg-transparent border-none font-semibold hover:underline" onClick={() => setShowPastAdoptions((v) => !v)}>
{showPastAdoptions ? "Hide" : "Show"} past adoptions ({pastAdoptions.length})
</button>
{showPastAdoptions && (
<div className="appt-list appt-list--past">
<div className="flex flex-col gap-3 mt-3 opacity-75">
{pastAdoptions.map((a) => (
<div key={a.adoptionId} className="appt-card appt-card--past">
<div className="appt-card-header">
<span className="appt-card-service">{a.petName}</span>
<span className={`appt-card-status appt-card-status--${a.adoptionStatus?.toLowerCase()}`}>
<div key={a.adoptionId} className="bg-[#f9f9f9] rounded-xl p-4 border border-[#eee]">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-[#333]">{a.petName}</span>
<span className={`text-xs font-semibold rounded-full px-2.5 py-1 appt-card-status--${a.adoptionStatus?.toLowerCase()}`}>
{a.adoptionStatus}
</span>
</div>
<div className="appt-card-details">
<div className="flex gap-4 text-[0.85rem] text-[#666] flex-wrap">
<span>{a.sourceStoreName}</span>
<span>{a.adoptionDate}</span>
</div>