Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ function App() {
);
return;
}
// validate email format if provided
if (form.creatorEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.creatorEmail)) {
Copy link
Copy Markdown
Owner

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?

setFormError("Please enter a valid email address.");
return;
}
// validate start < end
if (form.isAllDay) {
if (form.eDate <= form.sDate) {
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or... we can keep this CollapsibleSection and rename it to RSVP.
And put all RSVP related stuff here.

  1. An email address
  2. OR a link to an external page and open it in a new tab
  3. OR an iframe in a modal

</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">
Expand Down
145 changes: 145 additions & 0 deletions src/RSVP.tsx
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);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of "Someone" we can say "I".
Because it's an email from the sender.

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 ..."
We can have different sentences per answer.

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:
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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}` : ''}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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}` : ''}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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}`;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The 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:
❤️ Calf 🐮📅


// 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>
</>
);
}
9 changes: 8 additions & 1 deletion src/Share.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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") || "",
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say "e" is enough. (First come, first serve :P)
Also "email" instead of "creatorEmail"

};
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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" />
Expand Down Expand Up @@ -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">
Expand Down
3 changes: 3 additions & 0 deletions src/eventForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type EventForm = {
isOnline: boolean;
isAllDay: boolean;
password: string;
creatorEmail: string;
};

export type EventQS = {
Expand All @@ -25,6 +26,7 @@ export type EventQS = {
timezone: string;
isOnline: boolean;
isAllDay: boolean;
creatorEmail: string;
};

const nowTime = function () {
Expand Down Expand Up @@ -69,4 +71,5 @@ export const initialForm: EventForm = {
isOnline: false,
isAllDay: false,
password: "",
creatorEmail: "",
};
1 change: 1 addition & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down