diff --git a/src/App.tsx b/src/App.tsx index 7c0a1db..a0bc009 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -103,6 +103,11 @@ function App() { ); return; } + // validate email format if provided + if (form.creatorEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.creatorEmail)) { + setFormError("Please enter a valid email address."); + return; + } // validate start < end if (form.isAllDay) { if (form.eDate <= form.sDate) { @@ -418,7 +423,45 @@ function App() { - {/* Collapsible section for password protection */} + {/* Collapsible section for organizer email */} + + Organizer email (optional) + + } + > + + Add an organizer email + + } + className="my-4 text-left" + description={ +
+ We will use this email to enable RSVP functionality for attendees. +
+ Attendees can respond with Yes, No, or Maybe to your event. +
+ It won't be exposed unless you share the event. +
+ } + /> + + setForm((f) => ({ ...f, creatorEmail: e.target.value })) + } + /> +
+ + {/* Collapsible section for password protection */} diff --git a/src/RSVP.tsx b/src/RSVP.tsx new file mode 100644 index 0000000..0db5578 --- /dev/null +++ b/src/RSVP.tsx @@ -0,0 +1,145 @@ +import {useState} from "react"; +import {Button, Input, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react"; + +interface RSVPProps { + event: { + title: string; + description: string; + location: string; + sDate: string; + creatorEmail: string; + }; +} + +export default function RSVP({ event }: RSVPProps) { + const [rsvpResponse, setRsvpResponse] = useState<"Yes" | "No" | "Maybe" | null>(null); + const [rsvpSubmitted, setRsvpSubmitted] = useState(false); + const [showRsvpModal, setShowRsvpModal] = useState(false); + const [pendingResponse, setPendingResponse] = useState<"Yes" | "No" | "Maybe" | null>(null); + const [userName, setUserName] = useState(""); + + const handleRsvpClick = (response: "Yes" | "No" | "Maybe") => { + setPendingResponse(response); + setShowRsvpModal(true); + }; + + const handleRsvpSubmit = () => { + if (!pendingResponse || !event.creatorEmail) return; + + const name = userName.trim() || "Someone"; + const responseText = pendingResponse; + + // Create mailto link + const subject = `RSVP: ${responseText} - ${event.title}`; + const body = `Hi, + +${name} is responding to your event: "${event.title}" + +Response: ${responseText} + +Event Details: +${event.description ? `Description: ${event.description}` : ''} +${event.location ? `Location: ${event.location}` : ''} +${event.sDate ? `Date: ${event.sDate}` : ''} + +Best regards, +${name}`; + + // Open mailto link + window.location.href = `mailto:${event.creatorEmail}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`; + + // Close modal and mark as submitted + setShowRsvpModal(false); + setRsvpResponse(pendingResponse); + setRsvpSubmitted(true); + setUserName(""); + setPendingResponse(null); + }; + + const handleRsvpCancel = () => { + setShowRsvpModal(false); + setPendingResponse(null); + setUserName(""); + }; + + if (!event.creatorEmail) { + return null; + } + + return ( + <> +
+
Will you attend this event?
+
+ Let the organizer know if you'll be coming +
+ {!rsvpSubmitted ? ( +
+ + + +
+ ) : ( +
+
+ ✓ RSVP submitted: {rsvpResponse} +
+ +
+ )} +
+ + + + + RSVP to Event + +

+ Your response: {pendingResponse} +

+ setUserName(e.target.value)} + className="w-full" + /> +

+ If left empty, we'll use "Someone" in the email +

+
+ + + +
+
+ + ); +} diff --git a/src/Share.tsx b/src/Share.tsx index c560f4b..ad62ef7 100644 --- a/src/Share.tsx +++ b/src/Share.tsx @@ -3,6 +3,7 @@ import { generateICal } from "./icalUtils"; import { ArrowDownTrayIcon, CalendarDaysIcon } from "@heroicons/react/16/solid"; import { Link } from "@heroui/react"; import { useState } from "react"; +import RSVP from "./RSVP"; import { dateFromParts, decryptString, @@ -31,6 +32,7 @@ function parseStandardParams(): EventQS { timezone: params.get("tz") || "", isOnline: typeof params.get("o") === "string", isAllDay: typeof params.get("a") === "string", + creatorEmail: params.get("ce") || "", }; } @@ -59,6 +61,7 @@ export default function Share() { timezone: data.tz ?? "", isOnline: data.o === "1", isAllDay: data.a === "1", + creatorEmail: data.ce ?? "", }); setUnlocked(true); } catch { @@ -214,7 +217,7 @@ export default function Share() {
-
+
@@ -270,8 +273,12 @@ export default function Share() {
)} + + {/* RSVP Section */} +
+
Add to your calendar:
diff --git a/src/eventForm.ts b/src/eventForm.ts index 2a67f41..b011e35 100644 --- a/src/eventForm.ts +++ b/src/eventForm.ts @@ -12,6 +12,7 @@ export type EventForm = { isOnline: boolean; isAllDay: boolean; password: string; + creatorEmail: string; }; export type EventQS = { @@ -25,6 +26,7 @@ export type EventQS = { timezone: string; isOnline: boolean; isAllDay: boolean; + creatorEmail: string; }; const nowTime = function () { @@ -69,4 +71,5 @@ export const initialForm: EventForm = { isOnline: false, isAllDay: false, password: "", + creatorEmail: "", }; diff --git a/src/helpers.ts b/src/helpers.ts index f56d58d..6e11fec 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -128,6 +128,7 @@ export function formToRecord( tz: form.timezone, o: form.isOnline ? "" : undefined, a: form.isAllDay ? "" : undefined, + ce: form.creatorEmail || undefined, }; return Object.fromEntries(