Page updates
This commit is contained in:
@ -4,6 +4,7 @@ import "../styles/global.css";
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
attending: boolean | null;
|
||||
dietaryRestrictions: string;
|
||||
notes: string;
|
||||
}
|
||||
@ -16,6 +17,7 @@ interface ApiResponse {
|
||||
const RSVPForm = () => {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: "",
|
||||
attending: null,
|
||||
dietaryRestrictions: "",
|
||||
notes: "",
|
||||
});
|
||||
@ -23,7 +25,7 @@ const RSVPForm = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const isFormValid = formData.name.trim().length > 0;
|
||||
const isFormValid = formData.name.trim().length > 0 && formData.attending !== null;
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@ -34,9 +36,8 @@ const RSVPForm = () => {
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
// Create an AbortController for timeout
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
|
||||
const response = await fetchWithAuth("/api/rsvp", {
|
||||
method: "POST",
|
||||
@ -63,6 +64,7 @@ const RSVPForm = () => {
|
||||
|
||||
setFormData({
|
||||
name: "",
|
||||
attending: null,
|
||||
dietaryRestrictions: "",
|
||||
notes: "",
|
||||
});
|
||||
@ -80,7 +82,6 @@ const RSVPForm = () => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto p-6">
|
||||
<div className="card bg-base-100 shadow-xl">
|
||||
@ -147,30 +148,68 @@ const RSVPForm = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dietary Restrictions Input */}
|
||||
{/* Attendance Selection */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="dietary" className="label">
|
||||
<span className="label-text">Dietary Restrictions</span>
|
||||
<label className="label">
|
||||
<span className="label-text">Will you be attending? *</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="dietary"
|
||||
value={formData.dietaryRestrictions}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
dietaryRestrictions: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Please list any dietary restrictions or allergies"
|
||||
className="textarea textarea-bordered h-24"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
checked={formData.attending === true}
|
||||
onChange={() => setFormData(prev => ({ ...prev, attending: true }))}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<span>Yes, I'll be there!</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
className="radio radio-primary"
|
||||
checked={formData.attending === false}
|
||||
onChange={() => setFormData(prev => ({ ...prev, attending: false }))}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<span>No, I can't make it</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes Input */}
|
||||
{/* Only show these fields if attending */}
|
||||
{formData.attending && (
|
||||
<>
|
||||
{/* Dietary Restrictions Input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="dietary" className="label">
|
||||
<span className="label-text">Dietary Restrictions</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="dietary"
|
||||
value={formData.dietaryRestrictions}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
dietaryRestrictions: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Please list any dietary restrictions or allergies"
|
||||
className="textarea textarea-bordered h-24"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Notes Input - shown for both attending and not attending */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="notes" className="label">
|
||||
<span className="label-text">Anything else you'd like Ix and Tasha to know?</span>
|
||||
<span className="label-text">
|
||||
{formData.attending
|
||||
? "Anything else you'd like Ix and Tasha to know?"
|
||||
: "Would you like to leave a message for Ix and Tasha?"}
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="notes"
|
||||
@ -181,7 +220,9 @@ const RSVPForm = () => {
|
||||
notes: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="e.g. only coming for ceremony, bringing a +1, etc."
|
||||
placeholder={formData.attending
|
||||
? "e.g. attending just the ceremony or reception, etc."
|
||||
: "Optional message"}
|
||||
className="textarea textarea-bordered h-24"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
@ -190,7 +231,7 @@ const RSVPForm = () => {
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
className={`btn w-full ${formData.attending ? 'btn-primary' : 'btn-secondary'}`}
|
||||
disabled={!isFormValid || isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
@ -198,8 +239,12 @@ const RSVPForm = () => {
|
||||
<span className="loading loading-spinner"></span>
|
||||
Submitting...
|
||||
</>
|
||||
) : formData.attending ? (
|
||||
"Yes, I'll Be There!"
|
||||
) : formData.attending === false ? (
|
||||
"Send My Regrets"
|
||||
) : (
|
||||
"I'll Be There!"
|
||||
"Submit RSVP"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
@ -55,13 +55,37 @@ const RSVPManager = () => {
|
||||
return <div className="text-red-500 text-center">Error: {error}</div>;
|
||||
}
|
||||
|
||||
const attending = rsvpList.filter(rsvp => rsvp.attending === true);
|
||||
const notAttending = rsvpList.filter(rsvp => rsvp.attending === false);
|
||||
const noResponse = rsvpList.filter(rsvp => rsvp.attending === undefined || rsvp.attending === null);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<div className="stats shadow mb-8 w-full">
|
||||
<div className="stat">
|
||||
<div className="stat-title">Total RSVPs</div>
|
||||
<div className="stat-value">{rsvpList.length}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-title">Attending</div>
|
||||
<div className="stat-value text-success">{attending.length}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-title">Not Attending</div>
|
||||
<div className="stat-value text-error">{notAttending.length}</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-title">No Response</div>
|
||||
<div className="stat-value text-warning">{noResponse.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Dietary Restrictions</th>
|
||||
<th>Notes</th>
|
||||
<th>Timestamp</th>
|
||||
@ -71,7 +95,18 @@ const RSVPManager = () => {
|
||||
{rsvpList.map((rsvp, index) => (
|
||||
<tr key={index}>
|
||||
<td>{rsvp.name}</td>
|
||||
<td>{rsvp.dietaryRestrictions || "None"}</td>
|
||||
<td>
|
||||
<div className={`badge ${
|
||||
rsvp.attending === true ? 'badge-success' :
|
||||
rsvp.attending === false ? 'badge-error' :
|
||||
'badge-warning'
|
||||
}`}>
|
||||
{rsvp.attending === true ? 'Attending' :
|
||||
rsvp.attending === false ? 'Not Attending' :
|
||||
'No Response'}
|
||||
</div>
|
||||
</td>
|
||||
<td>{rsvp.attending ? (rsvp.dietaryRestrictions || "None") : "-"}</td>
|
||||
<td>{(rsvp.notes && rsvp.notes !== "undefined") ? rsvp.notes.trim() : "None"}</td>
|
||||
<td>{new Date(rsvp.timestamp).toLocaleString()}</td>
|
||||
</tr>
|
||||
@ -79,10 +114,6 @@ const RSVPManager = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
<p>Total RSVPs: {rsvpList.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ export interface RegistryItem {
|
||||
|
||||
export interface RSVPItem {
|
||||
name: string;
|
||||
attending: boolean | null;
|
||||
dietaryRestrictions: string;
|
||||
notes: string;
|
||||
timestamp: string;
|
||||
|
@ -1,47 +0,0 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import RSVP from "../components/RSVP.tsx";
|
||||
import SignIn from "../components/SignIn.tsx";
|
||||
---
|
||||
|
||||
<Layout title="RSVP">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="text-center text-4xl">
|
||||
Please RSVP using the form below:
|
||||
</div>
|
||||
|
||||
<div id="auth-container">
|
||||
<SignIn client:load onSuccess={() => {}} requiredRole="guest" />
|
||||
</div>
|
||||
|
||||
<div id="content-container" class="hidden">
|
||||
<RSVP client:load />
|
||||
<div class="flex flex-row gap-2 justify-center items-center mt-4">
|
||||
<a class="btn btn-primary" href="/">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
import { hasRole, isAuthenticated } from "../utils/auth-client";
|
||||
|
||||
function updateVisibility() {
|
||||
const authContainer = document.getElementById("auth-container");
|
||||
const contentContainer = document.getElementById("content-container");
|
||||
|
||||
if (isAuthenticated() && hasRole("guest")) {
|
||||
authContainer?.classList.add("hidden");
|
||||
contentContainer?.classList.remove("hidden");
|
||||
} else {
|
||||
authContainer?.classList.remove("hidden");
|
||||
contentContainer?.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Check auth state on page load
|
||||
updateVisibility();
|
||||
|
||||
// Add event listener for custom event from SignIn component
|
||||
document.addEventListener("auth-success", updateVisibility);
|
||||
</script>
|
Reference in New Issue
Block a user