Skip to content
Merged
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
478 changes: 478 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"date-fns": "^3.6.0",
"dexie": "^4.0.11",
"embla-carousel-react": "^8.3.0",
"googleapis": "^144.0.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.446.0",
"next-themes": "^0.3.0",
Expand Down
185 changes: 185 additions & 0 deletions src/components/EmailDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Cross1Icon, CheckIcon } from "@radix-ui/react-icons";
import {
Dialog,
DialogContent,
DialogTitle,
DialogDescription,
} from "@radix-ui/react-dialog";
import { Meeting } from "@/types";
import { ampEmailService } from "@/services/ampEmailService";

interface EmailDialogProps {
meetings: Meeting[];
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}

export function EmailDialog({
meetings,
open,
onOpenChange,
onSuccess,
}: EmailDialogProps) {
const [recipients, setRecipients] = useState<{ email: string }[]>([
{ email: "" },
]);
const [isSending, setIsSending] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);

const resetDialog = () => {
setRecipients([{ email: "" }]);
setIsSending(false);
setIsSuccess(false);
};

const isValidEmail = (email: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim());
};

const hasValidEmails = () => {
const filledEmails = recipients.filter((r) => r.email.trim());
return (
filledEmails.length > 0 &&
filledEmails.every((r) => isValidEmail(r.email))
);
};

const handleSendEmail = async () => {
const validRecipients = recipients.filter(
(r) => r.email.trim() && isValidEmail(r.email)
);
if (validRecipients.length === 0) {
alert("Please add at least one valid email address");
return;
}

try {
setIsSending(true);
await ampEmailService.sendEmail(meetings, validRecipients);
setIsSuccess(true);
onSuccess?.();
setTimeout(() => {
onOpenChange(false);
resetDialog();
}, 2000);
} catch (error) {
console.error("Failed to send email:", error);
alert("Failed to send email. Please try again.");
} finally {
setIsSending(false);
}
};

const addRecipient = () => {
setRecipients([...recipients, { email: "" }]);
};

const removeRecipient = (index: number) => {
setRecipients(recipients.filter((_, i) => i !== index));
};

return (
<Dialog open={open} onOpenChange={onOpenChange}>
{open && <div className="fixed inset-0 bg-black/50" />}
<DialogContent className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-lg p-6 w-[480px] shadow-lg z-50">
<DialogTitle className="text-2xl font-normal mb-6">
Send Report
</DialogTitle>
<DialogDescription className="sr-only">
Add email recipients to send the meeting report
</DialogDescription>
<div className="space-y-3">
{recipients.map((recipient, index) => (
<div key={index} className="flex items-center gap-2">
<div className="flex items-center gap-2 w-full">
<span className="text-gray-400">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 12C14.21 12 16 10.21 16 8C16 5.79 14.21 4 12 4C9.79 4 8 5.79 8 8C8 10.21 9.79 12 12 12Z"
fill="currentColor"
/>
<path
d="M12 14C9.33 14 4 15.34 4 18V20H20V18C20 15.34 14.67 14 12 14Z"
fill="currentColor"
/>
</svg>
</span>
<Input
type="email"
placeholder="Enter email address"
value={recipient.email}
onChange={(e) => {
const newRecipients = [...recipients];
newRecipients[index].email = e.target.value;
setRecipients(newRecipients);
}}
className="border-0 border-b border-gray-200 rounded-none focus:ring-0 px-2 py-2"
aria-label={`Recipient ${index + 1} email`}
required
/>
</div>
{recipients.length > 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => removeRecipient(index)}
className="p-1 hover:bg-transparent"
aria-label={`Remove recipient ${index + 1}`}
>
<Cross1Icon className="h-4 w-4" />
</Button>
)}
</div>
))}
<Button
variant="ghost"
type="button"
onClick={addRecipient}
className="w-full text-gray-600 justify-start px-0 hover:bg-transparent"
>
<span className="text-xl mr-2">+</span> Add another recipient
</Button>
</div>
<div className="flex justify-end gap-3 mt-8">
<Button
variant="ghost"
onClick={() => onOpenChange(false)}
className="text-gray-600"
>
Cancel
</Button>
<Button
onClick={handleSendEmail}
disabled={isSending || isSuccess || !hasValidEmails()}
className={`${
isSuccess
? "bg-green-600 hover:bg-green-600"
: "bg-gray-600 hover:bg-gray-700"
} flex items-center gap-2`}
>
{isSuccess ? (
<>
<CheckIcon className="h-4 w-4" />
Email Sent
</>
) : isSending ? (
"Sending..."
) : (
"Send Report"
)}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
19 changes: 16 additions & 3 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { Meeting } from "@/types";
import { importCalendarData } from "@/services/calendarService";
import { googleCalendarService } from "@/services/googleCalendarService";
import { importGoogleCalendar } from "@/services/calendarService";
import { sendEmail } from "@/services/emailService";
import { db } from "@/services/db";
import { ExportInstructions } from "./ExportInstructions";
import { EmailDialog } from "./EmailDialog";
// import { mockMeetings } from "@/mockData";

interface HeaderProps {
Expand All @@ -32,6 +32,8 @@ export function Header({
HeaderProps) {
const [isImporting, setIsImporting] = useState(false);
const [isClearing, setIsClearing] = useState(false);
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
const [showEmailSuccess, setShowEmailSuccess] = useState(false);

const handleFileUpload = async (
event: React.ChangeEvent<HTMLInputElement>
Expand Down Expand Up @@ -91,9 +93,9 @@ HeaderProps) {
className="hidden"
onChange={handleFileUpload}
/>
<Button variant="outline" onClick={() => sendEmail(meetings)}>
<Button variant="outline" onClick={() => setEmailDialogOpen(true)}>
<EnvelopeClosedIcon className="w-4 h-4 mr-2" />
Send Email
{showEmailSuccess ? "Email Sent!" : "Send Email"}
</Button>
<Button
variant="outline"
Expand All @@ -103,6 +105,7 @@ HeaderProps) {
>
{isClearing ? "Clearing..." : "Clear Data"}
</Button>

{/* Removing mock data from the UI for now */}
{/* <Button
variant="outline"
Expand Down Expand Up @@ -137,6 +140,16 @@ HeaderProps) {
{isImporting ? "Importing..." : "Import from Google"}
</Button>
</div>

<EmailDialog
meetings={meetings}
open={emailDialogOpen}
onOpenChange={setEmailDialogOpen}
onSuccess={() => {
setShowEmailSuccess(true);
setTimeout(() => setShowEmailSuccess(false), 2000);
}}
/>
</div>
);
}
5 changes: 3 additions & 2 deletions src/components/Plan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function Plan() {
});
};

const handleMeetingAction = (action: string, meetingId: string) => {
const handleMeetingAction = async (action: string, meetingId: string) => {
const store = useSettingsStore.getState();
const currentStatus = store.meetingStatus?.[meetingId] || {
needsCancel: false,
Expand Down Expand Up @@ -100,7 +100,7 @@ export function Plan() {
: currentStatus.prepRequired,
};

store.setMeetingStatus?.(meetingId, newStatus);
await store.setMeetingStatus(meetingId, newStatus);
};

const handleDragEnd = (result: DropResult) => {
Expand All @@ -116,6 +116,7 @@ export function Plan() {
}));

setLocalMeetings(updatedItems);
useSettingsStore.getState().updateMeetings(updatedItems);
updateStats(updatedItems);
};

Expand Down
Loading
Loading