-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Add RSVP functionality with email integration #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
54c1d0e
7393c75
ef16598
3d86d1c
d604d8c
3793c69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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() { | |
| </div> | ||
| </div> | ||
|
|
||
| {/* Collapsible section for password protection */} | ||
| {/* Collapsible section for organizer email */} | ||
| <CollapsibleSection | ||
| initiallyOpen={!!form.creatorEmail} | ||
| title={ | ||
| <span className="text-sm"> | ||
| Organizer email (optional) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or... we can keep this
|
||
| </span> | ||
| } | ||
| > | ||
| <Alert | ||
| color="primary" | ||
| variant="faded" | ||
| title={ | ||
| <div className="mb-2 font-bold"> | ||
| Add an organizer email | ||
| </div> | ||
| } | ||
| className="my-4 text-left" | ||
| description={ | ||
| <div> | ||
| We will use this email to enable RSVP functionality for attendees. | ||
| <br /> | ||
| Attendees can respond with Yes, No, or Maybe to your event. | ||
| <br /> | ||
| It won't be exposed unless you share the event. | ||
| </div> | ||
| } | ||
| /> | ||
| <Input | ||
| type="email" | ||
| placeholder="Enter organizer email (optional)" | ||
| value={form.creatorEmail} | ||
| onChange={(e) => | ||
| setForm((f) => ({ ...f, creatorEmail: e.target.value })) | ||
| } | ||
| /> | ||
| </CollapsibleSection> | ||
|
|
||
| {/* Collapsible section for password protection */} | ||
| <CollapsibleSection | ||
| title={ | ||
| <span className="text-sm"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add a fourth option, to contact |
||
| 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"; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of "Someone" we can say "I". And surely we need to rephrase the email body a bit. Or totally, we can skip the question about user's name and always respond with "I ..." |
||
| 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: | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for this line |
||
| ${event.description ? `Description: ${event.description}` : ''} | ||
| ${event.location ? `Location: ${event.location}` : ''} | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, each line prints an empty line even if they are not there! |
||
| ${event.sDate ? `Date: ${event.sDate}` : ''} | ||
|
|
||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can be good to add the link to the event somewhere in the email. |
||
| Best regards, | ||
| ${name}`; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Put the poor calf somewhere at the bottom: Something like this is enough: |
||
|
|
||
| // 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 ( | ||
| <> | ||
| <div className="mt-4 pt-4 border-t border-gray-200"> | ||
| <div className="font-semibold text-gray-800 mb-3">Will you attend this event?</div> | ||
| <div className="text-sm text-gray-600 mb-4"> | ||
| Let the organizer know if you'll be coming | ||
| </div> | ||
| {!rsvpSubmitted ? ( | ||
| <div className="flex gap-2"> | ||
| <Button | ||
| onPress={() => handleRsvpClick("Yes")} | ||
| className="flex-1 bg-green-600 hover:bg-green-700 text-white text-sm font-medium py-2 px-3 rounded transition-colors" | ||
| > | ||
| Yes | ||
| </Button> | ||
| <Button | ||
| onPress={() => handleRsvpClick("Maybe")} | ||
| className="flex-1 bg-yellow-500 hover:bg-yellow-600 text-white text-sm font-medium py-2 px-3 rounded transition-colors" | ||
| > | ||
| Maybe | ||
| </Button> | ||
| <Button | ||
| onPress={() => handleRsvpClick("No")} | ||
| className="flex-1 bg-red-600 hover:bg-red-700 text-white text-sm font-medium py-2 px-3 rounded transition-colors" | ||
| > | ||
| No | ||
| </Button> | ||
| </div> | ||
| ) : ( | ||
| <div className="text-center"> | ||
| <div className="text-green-600 font-medium mb-2"> | ||
| ✓ RSVP submitted: {rsvpResponse} | ||
| </div> | ||
| <Button | ||
| onPress={() => { | ||
| setRsvpSubmitted(false); | ||
| setRsvpResponse(null); | ||
| }} | ||
| className="text-sm text-gray-500 hover:text-gray-700 underline" | ||
| > | ||
| Change response | ||
| </Button> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
|
|
||
| <Modal | ||
| isOpen={showRsvpModal} | ||
| onOpenChange={handleRsvpCancel} | ||
| > | ||
| <ModalContent> | ||
| <ModalHeader className="flex flex-col gap-1">RSVP to Event</ModalHeader> | ||
| <ModalBody> | ||
| <p className="text-sm text-gray-600"> | ||
| Your response: <span className="font-medium">{pendingResponse}</span> | ||
| </p> | ||
| <Input | ||
| placeholder="Your name (optional)" | ||
| value={userName} | ||
| onChange={(e) => setUserName(e.target.value)} | ||
| className="w-full" | ||
| /> | ||
| <p className="text-xs text-gray-500"> | ||
| If left empty, we'll use "Someone" in the email | ||
| </p> | ||
| </ModalBody> | ||
| <ModalFooter> | ||
| <Button color="primary" onPress={handleRsvpSubmit}> | ||
| Send email | ||
| </Button> | ||
| </ModalFooter> | ||
| </ModalContent> | ||
| </Modal> | ||
| </> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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") || "", | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd say "e" is enough. (First come, first serve :P) |
||
| }; | ||
| } | ||
|
|
||
|
|
@@ -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() { | |
| <div className="hidden xs:block"> | ||
| <CalendarDaysIcon className="w-35 h-35 text-gray-500" /> | ||
| </div> | ||
| <div className="flex flex-col gap-2 justify-start text-left align-middle "> | ||
| <div className="flex flex-col gap-2 justify-start text-left align-middle"> | ||
| <div className="flex flex-row items-center gap-3"> | ||
| <div className="xs:hidden"> | ||
| <CalendarDaysIcon className="w-10 h-10 text-gray-700" /> | ||
|
|
@@ -270,8 +273,12 @@ export default function Share() { | |
| </div> | ||
| </> | ||
| )} | ||
|
|
||
| {/* RSVP Section */} | ||
| <RSVP event={event} /> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="font-semibold">Add to your calendar:</div> | ||
|
|
||
| <div className="grid xs:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3 w-full"> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type of the field is email, so basically no validation needed, no?