Page updates
This commit is contained in:
@ -4,6 +4,7 @@ import "../styles/global.css";
|
|||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
name: string;
|
name: string;
|
||||||
|
attending: boolean | null;
|
||||||
dietaryRestrictions: string;
|
dietaryRestrictions: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
}
|
}
|
||||||
@ -16,6 +17,7 @@ interface ApiResponse {
|
|||||||
const RSVPForm = () => {
|
const RSVPForm = () => {
|
||||||
const [formData, setFormData] = useState<FormData>({
|
const [formData, setFormData] = useState<FormData>({
|
||||||
name: "",
|
name: "",
|
||||||
|
attending: null,
|
||||||
dietaryRestrictions: "",
|
dietaryRestrictions: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
});
|
});
|
||||||
@ -23,7 +25,7 @@ const RSVPForm = () => {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [success, setSuccess] = useState(false);
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -34,9 +36,8 @@ const RSVPForm = () => {
|
|||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create an AbortController for timeout
|
|
||||||
const controller = new AbortController();
|
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", {
|
const response = await fetchWithAuth("/api/rsvp", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -63,6 +64,7 @@ const RSVPForm = () => {
|
|||||||
|
|
||||||
setFormData({
|
setFormData({
|
||||||
name: "",
|
name: "",
|
||||||
|
attending: null,
|
||||||
dietaryRestrictions: "",
|
dietaryRestrictions: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
});
|
});
|
||||||
@ -80,7 +82,6 @@ const RSVPForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto p-6">
|
<div className="max-w-2xl mx-auto p-6">
|
||||||
<div className="card bg-base-100 shadow-xl">
|
<div className="card bg-base-100 shadow-xl">
|
||||||
@ -147,30 +148,68 @@ const RSVPForm = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dietary Restrictions Input */}
|
{/* Attendance Selection */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="dietary" className="label">
|
<label className="label">
|
||||||
<span className="label-text">Dietary Restrictions</span>
|
<span className="label-text">Will you be attending? *</span>
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<div className="flex gap-4">
|
||||||
id="dietary"
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
value={formData.dietaryRestrictions}
|
<input
|
||||||
onChange={(e) =>
|
type="radio"
|
||||||
setFormData((prev) => ({
|
className="radio radio-primary"
|
||||||
...prev,
|
checked={formData.attending === true}
|
||||||
dietaryRestrictions: e.target.value,
|
onChange={() => setFormData(prev => ({ ...prev, attending: true }))}
|
||||||
}))
|
disabled={isSubmitting}
|
||||||
}
|
/>
|
||||||
placeholder="Please list any dietary restrictions or allergies"
|
<span>Yes, I'll be there!</span>
|
||||||
className="textarea textarea-bordered h-24"
|
</label>
|
||||||
disabled={isSubmitting}
|
<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>
|
</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">
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="notes" className="label">
|
<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>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id="notes"
|
id="notes"
|
||||||
@ -181,7 +220,9 @@ const RSVPForm = () => {
|
|||||||
notes: e.target.value,
|
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"
|
className="textarea textarea-bordered h-24"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
@ -190,7 +231,7 @@ const RSVPForm = () => {
|
|||||||
{/* Submit Button */}
|
{/* Submit Button */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary w-full"
|
className={`btn w-full ${formData.attending ? 'btn-primary' : 'btn-secondary'}`}
|
||||||
disabled={!isFormValid || isSubmitting}
|
disabled={!isFormValid || isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
@ -198,8 +239,12 @@ const RSVPForm = () => {
|
|||||||
<span className="loading loading-spinner"></span>
|
<span className="loading loading-spinner"></span>
|
||||||
Submitting...
|
Submitting...
|
||||||
</>
|
</>
|
||||||
|
) : formData.attending ? (
|
||||||
|
"Yes, I'll Be There!"
|
||||||
|
) : formData.attending === false ? (
|
||||||
|
"Send My Regrets"
|
||||||
) : (
|
) : (
|
||||||
"I'll Be There!"
|
"Submit RSVP"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -55,13 +55,37 @@ const RSVPManager = () => {
|
|||||||
return <div className="text-red-500 text-center">Error: {error}</div>;
|
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 (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<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">
|
<div className="overflow-x-auto">
|
||||||
<table className="table w-full">
|
<table className="table w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
|
<th>Status</th>
|
||||||
<th>Dietary Restrictions</th>
|
<th>Dietary Restrictions</th>
|
||||||
<th>Notes</th>
|
<th>Notes</th>
|
||||||
<th>Timestamp</th>
|
<th>Timestamp</th>
|
||||||
@ -71,7 +95,18 @@ const RSVPManager = () => {
|
|||||||
{rsvpList.map((rsvp, index) => (
|
{rsvpList.map((rsvp, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>{rsvp.name}</td>
|
<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>{(rsvp.notes && rsvp.notes !== "undefined") ? rsvp.notes.trim() : "None"}</td>
|
||||||
<td>{new Date(rsvp.timestamp).toLocaleString()}</td>
|
<td>{new Date(rsvp.timestamp).toLocaleString()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -79,10 +114,6 @@ const RSVPManager = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<p>Total RSVPs: {rsvpList.length}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,7 @@ export interface RegistryItem {
|
|||||||
|
|
||||||
export interface RSVPItem {
|
export interface RSVPItem {
|
||||||
name: string;
|
name: string;
|
||||||
|
attending: boolean | null;
|
||||||
dietaryRestrictions: string;
|
dietaryRestrictions: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
timestamp: 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