fix contact form and appt ui
This commit is contained in:
@@ -383,6 +383,10 @@ function AppointmentsPage() {
|
||||
|
||||
const [showAddPetModal, setShowAddPetModal] = useState(false);
|
||||
const [cancellingId, setCancellingId] = useState(null);
|
||||
const [apptSearch, setApptSearch] = useState("");
|
||||
const [adoptionSearch, setAdoptionSearch] = useState("");
|
||||
const [showPastAppts, setShowPastAppts] = useState(false);
|
||||
const [showPastAdoptions, setShowPastAdoptions] = useState(false);
|
||||
|
||||
const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
|
||||
|
||||
@@ -966,79 +970,164 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
|
||||
<h2 className="appt-form-title">{canBookAppointments ? "Your Appointments" : "Appointments"}</h2>
|
||||
{loadingAppointments ? (
|
||||
<p className="appt-loading">Loading appointments...</p>
|
||||
) : appointments.length === 0 ? (
|
||||
<p className="appt-empty">No appointments yet.</p>
|
||||
) : (
|
||||
<div className="appt-list">
|
||||
{appointments.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()}`}>
|
||||
{a.appointmentStatus}
|
||||
</span>
|
||||
) : (() => {
|
||||
const activeAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() === "booked");
|
||||
const pastAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() !== "booked");
|
||||
const q = apptSearch.toLowerCase();
|
||||
const filteredActive = activeAppts.filter((a) =>
|
||||
!q || [a.serviceName, a.storeName, a.petName].some((v) => v?.toLowerCase().includes(q))
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className="appt-search"
|
||||
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>
|
||||
) : (
|
||||
<div className="appt-list">
|
||||
{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()}`}>
|
||||
{a.appointmentStatus}
|
||||
</span>
|
||||
</div>
|
||||
<div className="appt-card-details">
|
||||
<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="appt-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="appt-cancel-btn"
|
||||
disabled={cancellingId === a.appointmentId}
|
||||
onClick={() => handleCancelAppointment(a.appointmentId)}
|
||||
>
|
||||
{cancellingId === a.appointmentId ? "Cancelling..." : "Cancel"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="appt-card-details">
|
||||
<span>{a.storeName}</span>
|
||||
<span>{a.appointmentDate} at {formatTime(a.appointmentTime)}</span>
|
||||
)}
|
||||
{pastAppts.length > 0 && (
|
||||
<div className="appt-past-section">
|
||||
<button className="appt-past-toggle" onClick={() => setShowPastAppts((v) => !v)}>
|
||||
{showPastAppts ? "Hide" : "Show"} past appointments ({pastAppts.length})
|
||||
</button>
|
||||
{showPastAppts && (
|
||||
<div className="appt-list appt-list--past">
|
||||
{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()}`}>
|
||||
{a.appointmentStatus}
|
||||
</span>
|
||||
</div>
|
||||
<div className="appt-card-details">
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{a.petName && (
|
||||
<div className="appt-card-pets">
|
||||
Pet: {a.petName}
|
||||
</div>
|
||||
)}
|
||||
{a.appointmentStatus?.toLowerCase() === "booked" && (
|
||||
<div className="appt-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="appt-cancel-btn"
|
||||
disabled={cancellingId === a.appointmentId}
|
||||
onClick={() => handleCancelAppointment(a.appointmentId)}
|
||||
>
|
||||
{cancellingId === a.appointmentId ? "Cancelling..." : "Cancel"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
<h2 className="appt-form-title" style={{ marginTop: "2rem" }}>{canBookAppointments ? "Your Adoptions" : "Adoptions"}</h2>
|
||||
{loadingAdoptions ? (
|
||||
<p className="appt-loading">Loading adoptions...</p>
|
||||
) : adoptions.length === 0 ? (
|
||||
<p className="appt-empty">No adoption requests yet.</p>
|
||||
) : (
|
||||
<div className="appt-list">
|
||||
{adoptions.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()}`}>
|
||||
{a.adoptionStatus}
|
||||
</span>
|
||||
) : (() => {
|
||||
const activeAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() === "pending");
|
||||
const pastAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() !== "pending");
|
||||
const q = adoptionSearch.toLowerCase();
|
||||
const filteredActive = activeAdoptions.filter((a) =>
|
||||
!q || [a.petName, a.sourceStoreName].some((v) => v?.toLowerCase().includes(q))
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className="appt-search"
|
||||
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>
|
||||
) : (
|
||||
<div className="appt-list">
|
||||
{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()}`}>
|
||||
{a.adoptionStatus}
|
||||
</span>
|
||||
</div>
|
||||
<div className="appt-card-details">
|
||||
<span>{a.sourceStoreName}</span>
|
||||
<span>{a.adoptionDate}</span>
|
||||
</div>
|
||||
<div className="appt-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="appt-cancel-btn"
|
||||
disabled={cancellingId === a.adoptionId}
|
||||
onClick={() => handleCancelAdoption(a.adoptionId)}
|
||||
>
|
||||
{cancellingId === a.adoptionId ? "Cancelling..." : "Cancel"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="appt-card-details">
|
||||
<span>{a.sourceStoreName}</span>
|
||||
<span>{a.adoptionDate}</span>
|
||||
)}
|
||||
{pastAdoptions.length > 0 && (
|
||||
<div className="appt-past-section">
|
||||
<button className="appt-past-toggle" onClick={() => setShowPastAdoptions((v) => !v)}>
|
||||
{showPastAdoptions ? "Hide" : "Show"} past adoptions ({pastAdoptions.length})
|
||||
</button>
|
||||
{showPastAdoptions && (
|
||||
<div className="appt-list appt-list--past">
|
||||
{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()}`}>
|
||||
{a.adoptionStatus}
|
||||
</span>
|
||||
</div>
|
||||
<div className="appt-card-details">
|
||||
<span>{a.sourceStoreName}</span>
|
||||
<span>{a.adoptionDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{a.adoptionStatus?.toLowerCase() === "pending" && (
|
||||
<div className="appt-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="appt-cancel-btn"
|
||||
disabled={cancellingId === a.adoptionId}
|
||||
onClick={() => handleCancelAdoption(a.adoptionId)}
|
||||
>
|
||||
{cancellingId === a.adoptionId ? "Cancelling..." : "Cancel"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -38,6 +38,10 @@ export default function ContactPage() {
|
||||
|
||||
async function handleSend(e) {
|
||||
e.preventDefault();
|
||||
if (!token) {
|
||||
setSendError("Please log in to send a message.");
|
||||
return;
|
||||
}
|
||||
setSending(true);
|
||||
setSendError(null);
|
||||
try {
|
||||
@@ -72,44 +76,42 @@ export default function ContactPage() {
|
||||
<p>Phone: (03) 9000 0000</p>
|
||||
<p>Hours: Mon–Sat, 9:00 AM – 6:00 PM</p>
|
||||
|
||||
{token && (
|
||||
<div className="contact-form-section">
|
||||
<h3>Send Us a Message</h3>
|
||||
{sendSuccess ? (
|
||||
<p className="contact-success">Your message has been sent. We'll be in touch soon.</p>
|
||||
) : (
|
||||
<form className="auth-form" onSubmit={handleSend}>
|
||||
<label className="auth-label">
|
||||
Subject
|
||||
<input
|
||||
className="auth-input"
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
required
|
||||
maxLength={150}
|
||||
/>
|
||||
</label>
|
||||
<label className="auth-label">
|
||||
Message
|
||||
<textarea
|
||||
className="auth-input"
|
||||
style={{ resize: "vertical" }}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
required
|
||||
maxLength={2000}
|
||||
rows={5}
|
||||
/>
|
||||
</label>
|
||||
{sendError && <p className="contact-error">{sendError}</p>}
|
||||
<button className="auth-submit-btn" type="submit" disabled={sending}>
|
||||
{sending ? "Sending…" : "Send Message"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="contact-form-section">
|
||||
<h3>Send Us a Message</h3>
|
||||
{sendSuccess ? (
|
||||
<p className="contact-success">Your message has been sent. We'll be in touch soon.</p>
|
||||
) : (
|
||||
<form className="auth-form" onSubmit={handleSend}>
|
||||
<label className="auth-label">
|
||||
Subject
|
||||
<input
|
||||
className="auth-input"
|
||||
type="text"
|
||||
value={subject}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
required
|
||||
maxLength={150}
|
||||
/>
|
||||
</label>
|
||||
<label className="auth-label">
|
||||
Message
|
||||
<textarea
|
||||
className="auth-input"
|
||||
style={{ resize: "vertical" }}
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
required
|
||||
maxLength={2000}
|
||||
rows={5}
|
||||
/>
|
||||
</label>
|
||||
{sendError && <p className="contact-error">{sendError}</p>}
|
||||
<button className="auth-submit-btn" type="submit" disabled={sending}>
|
||||
{sending ? "Sending…" : "Send Message"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-card">
|
||||
|
||||
@@ -713,7 +713,7 @@ body {
|
||||
.info-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem 3rem;
|
||||
padding: 0 2rem 1.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1.5rem;
|
||||
@@ -737,6 +737,7 @@ body {
|
||||
padding-left: 1.2rem;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.info-card-grid {
|
||||
@@ -1713,6 +1714,49 @@ body {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.appt-search {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.appt-search:focus {
|
||||
outline: none;
|
||||
border-color: #e68672;
|
||||
}
|
||||
|
||||
.appt-past-section {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.appt-past-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.appt-past-toggle:hover {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.appt-list--past {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.appt-card--past {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
/* Adoption Pet Selection */
|
||||
|
||||
.appt-adopt-grid {
|
||||
|
||||
Reference in New Issue
Block a user