fix contact form and appt ui

This commit is contained in:
2026-04-15 23:35:41 -06:00
parent 4f6807b28f
commit 86a1b0f72c
3 changed files with 239 additions and 104 deletions

View File

@@ -383,6 +383,10 @@ function AppointmentsPage() {
const [showAddPetModal, setShowAddPetModal] = useState(false); const [showAddPetModal, setShowAddPetModal] = useState(false);
const [cancellingId, setCancellingId] = useState(null); 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"; 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> <h2 className="appt-form-title">{canBookAppointments ? "Your Appointments" : "Appointments"}</h2>
{loadingAppointments ? ( {loadingAppointments ? (
<p className="appt-loading">Loading appointments...</p> <p className="appt-loading">Loading appointments...</p>
) : appointments.length === 0 ? ( ) : (() => {
<p className="appt-empty">No appointments yet.</p> const activeAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() === "booked");
) : ( const pastAppts = appointments.filter((a) => a.appointmentStatus?.toLowerCase() !== "booked");
<div className="appt-list"> const q = apptSearch.toLowerCase();
{appointments.map((a) => ( const filteredActive = activeAppts.filter((a) =>
<div key={a.appointmentId} className="appt-card"> !q || [a.serviceName, a.storeName, a.petName].some((v) => v?.toLowerCase().includes(q))
<div className="appt-card-header"> );
<span className="appt-card-service">{a.serviceName}</span> return (
<span className={`appt-card-status appt-card-status--${a.appointmentStatus?.toLowerCase()}`}> <>
{a.appointmentStatus} <input
</span> 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>
<div className="appt-card-details"> )}
<span>{a.storeName}</span> {pastAppts.length > 0 && (
<span>{a.appointmentDate} at {formatTime(a.appointmentTime)}</span> <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> </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> <h2 className="appt-form-title" style={{ marginTop: "2rem" }}>{canBookAppointments ? "Your Adoptions" : "Adoptions"}</h2>
{loadingAdoptions ? ( {loadingAdoptions ? (
<p className="appt-loading">Loading adoptions...</p> <p className="appt-loading">Loading adoptions...</p>
) : adoptions.length === 0 ? ( ) : (() => {
<p className="appt-empty">No adoption requests yet.</p> const activeAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() === "pending");
) : ( const pastAdoptions = adoptions.filter((a) => a.adoptionStatus?.toLowerCase() !== "pending");
<div className="appt-list"> const q = adoptionSearch.toLowerCase();
{adoptions.map((a) => ( const filteredActive = activeAdoptions.filter((a) =>
<div key={a.adoptionId} className="appt-card"> !q || [a.petName, a.sourceStoreName].some((v) => v?.toLowerCase().includes(q))
<div className="appt-card-header"> );
<span className="appt-card-service">{a.petName}</span> return (
<span className={`appt-card-status appt-card-status--${a.adoptionStatus?.toLowerCase()}`}> <>
{a.adoptionStatus} <input
</span> 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>
<div className="appt-card-details"> )}
<span>{a.sourceStoreName}</span> {pastAdoptions.length > 0 && (
<span>{a.adoptionDate}</span> <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> </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> </div>
</section> </section>
</main> </main>

View File

@@ -38,6 +38,10 @@ export default function ContactPage() {
async function handleSend(e) { async function handleSend(e) {
e.preventDefault(); e.preventDefault();
if (!token) {
setSendError("Please log in to send a message.");
return;
}
setSending(true); setSending(true);
setSendError(null); setSendError(null);
try { try {
@@ -72,44 +76,42 @@ export default function ContactPage() {
<p>Phone: (03) 9000 0000</p> <p>Phone: (03) 9000 0000</p>
<p>Hours: MonSat, 9:00 AM 6:00 PM</p> <p>Hours: MonSat, 9:00 AM 6:00 PM</p>
{token && ( <div className="contact-form-section">
<div className="contact-form-section"> <h3>Send Us a Message</h3>
<h3>Send Us a Message</h3> {sendSuccess ? (
{sendSuccess ? ( <p className="contact-success">Your message has been sent. We&apos;ll be in touch soon.</p>
<p className="contact-success">Your message has been sent. We&apos;ll be in touch soon.</p> ) : (
) : ( <form className="auth-form" onSubmit={handleSend}>
<form className="auth-form" onSubmit={handleSend}> <label className="auth-label">
<label className="auth-label"> Subject
Subject <input
<input className="auth-input"
className="auth-input" type="text"
type="text" value={subject}
value={subject} onChange={(e) => setSubject(e.target.value)}
onChange={(e) => setSubject(e.target.value)} required
required maxLength={150}
maxLength={150} />
/> </label>
</label> <label className="auth-label">
<label className="auth-label"> Message
Message <textarea
<textarea className="auth-input"
className="auth-input" style={{ resize: "vertical" }}
style={{ resize: "vertical" }} value={body}
value={body} onChange={(e) => setBody(e.target.value)}
onChange={(e) => setBody(e.target.value)} required
required maxLength={2000}
maxLength={2000} rows={5}
rows={5} />
/> </label>
</label> {sendError && <p className="contact-error">{sendError}</p>}
{sendError && <p className="contact-error">{sendError}</p>} <button className="auth-submit-btn" type="submit" disabled={sending}>
<button className="auth-submit-btn" type="submit" disabled={sending}> {sending ? "Sending…" : "Send Message"}
{sending ? "Sending…" : "Send Message"} </button>
</button> </form>
</form> )}
)} </div>
</div>
)}
</div> </div>
<div className="info-card"> <div className="info-card">

View File

@@ -713,7 +713,7 @@ body {
.info-content { .info-content {
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
padding: 0 2rem 3rem; padding: 0 2rem 1.5rem;
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 1.5rem; gap: 1.5rem;
@@ -737,6 +737,7 @@ body {
padding-left: 1.2rem; padding-left: 1.2rem;
display: grid; display: grid;
gap: 0.5rem; gap: 0.5rem;
list-style-type: disc;
} }
.info-card-grid { .info-card-grid {
@@ -1713,6 +1714,49 @@ body {
cursor: default; 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 */ /* Adoption Pet Selection */
.appt-adopt-grid { .appt-adopt-grid {