Files
membership-fe/src/components/AddToCalendarButton.js
kayela 7694532d53 refactor: update styles in MembersDirectory and NewsletterArchive for consistency and improved theming
- Updated color classes to use CSS variables for better maintainability and theming.
- Refactored component styles in MembersDirectory.js to enhance visual consistency.
- Adjusted loading states and empty states in NewsletterArchive.js for improved user experience.
- Added new brand colors to tailwind.config.js for future use.
2026-01-12 20:10:33 -06:00

217 lines
7.5 KiB
JavaScript

import React, { useState } from 'react';
import { Calendar, ChevronDown, Download, RefreshCw } from 'lucide-react';
import { Button } from './ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from './ui/dropdown-menu';
/**
* AddToCalendarButton Component
* Universal calendar export button with support for:
* - Google Calendar (web link)
* - Microsoft Outlook (web link)
* - Apple Calendar / Outlook Desktop (webcal:// subscription)
* - Download .ics file (universal import)
* - Subscribe to personal feed (auto-sync)
*/
export default function AddToCalendarButton({
event = null, // Single event object { id, title, description, start_at, end_at, location }
showSubscribe = false, // Show "Subscribe to My Events" option
variant = "default", // Button variant: default, outline, ghost
size = "default" // Button size: default, sm, lg
}) {
const [isOpen, setIsOpen] = useState(false);
const backendUrl = process.env.REACT_APP_BACKEND_URL || 'http://localhost:8000';
// Format datetime for Google Calendar (YYYYMMDDTHHMMSSZ)
const formatGoogleDate = (dateString) => {
const date = new Date(dateString);
return date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
};
// Generate Google Calendar URL
const getGoogleCalendarUrl = () => {
if (!event) return null;
const params = new URLSearchParams({
action: 'TEMPLATE',
text: event.title,
dates: `${formatGoogleDate(event.start_at)}/${formatGoogleDate(event.end_at)}`,
details: event.description || '',
location: event.location || '',
});
return `https://calendar.google.com/calendar/render?${params.toString()}`;
};
// Generate Microsoft Outlook Web URL
const getOutlookWebUrl = () => {
if (!event) return null;
const startDate = new Date(event.start_at).toISOString();
const endDate = new Date(event.end_at).toISOString();
const params = new URLSearchParams({
path: '/calendar/action/compose',
rru: 'addevent',
subject: event.title,
startdt: startDate,
enddt: endDate,
body: event.description || '',
location: event.location || '',
});
return `https://outlook.live.com/calendar/0/deeplink/compose?${params.toString()}`;
};
// Get .ics download URL
const getIcsDownloadUrl = () => {
if (!event) return null;
return `${backendUrl}/api/events/${event.id}/download.ics`;
};
// Get webcal:// subscription URL (for Apple Calendar / Outlook Desktop)
const getWebcalUrl = () => {
// Convert http:// to webcal://
const webcalUrl = backendUrl.replace(/^https?:\/\//, 'webcal://');
return `${webcalUrl}/api/calendars/subscribe.ics`;
};
// Get all events download URL
const getAllEventsUrl = () => {
return `${backendUrl}/api/calendars/all-events.ics`;
};
// Handle calendar action
const handleCalendarAction = (action) => {
setIsOpen(false);
switch (action) {
case 'google':
window.open(getGoogleCalendarUrl(), '_blank');
break;
case 'outlook':
window.open(getOutlookWebUrl(), '_blank');
break;
case 'apple':
window.location.href = getWebcalUrl();
break;
case 'download':
window.open(getIcsDownloadUrl(), '_blank');
break;
case 'subscribe':
window.location.href = getWebcalUrl();
break;
case 'all-events':
window.open(getAllEventsUrl(), '_blank');
break;
default:
break;
}
};
return (
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger asChild>
<Button variant={variant} size={size} className="gap-2">
<Calendar className="h-4 w-4" />
Add to Calendar
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-64">
{event && (
<>
{/* Single Event Export Options */}
<div className="px-2 py-1.5 text-sm font-semibold text-var(--purple-ink)">
Add This Event
</div>
<DropdownMenuItem
onClick={() => handleCalendarAction('google')}
className="cursor-pointer"
>
<svg className="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
</svg>
Google Calendar
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleCalendarAction('outlook')}
className="cursor-pointer"
>
<svg className="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 2h14v20H7V2zm7 11c0 2.761-2.239 5-5 5H2c-.552 0-1-.448-1-1s.448-1 1-1h7c1.657 0 3-1.343 3-3V9c0-1.657-1.343-3-3-3H2c-.552 0-1-.448-1-1s.448-1 1-1h7c2.761 0 5 2.239 5 5v4z" />
</svg>
Outlook Web
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleCalendarAction('apple')}
className="cursor-pointer"
>
<svg className="h-4 w-4 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
</svg>
Apple Calendar
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleCalendarAction('download')}
className="cursor-pointer"
>
<Download className="h-4 w-4 mr-2" />
Download .ics File
</DropdownMenuItem>
{showSubscribe && <DropdownMenuSeparator />}
</>
)}
{showSubscribe && (
<>
{/* Subscription Options */}
<div className="px-2 py-1.5 text-sm font-semibold text-var(--purple-ink)">
Calendar Feeds
</div>
<DropdownMenuItem
onClick={() => handleCalendarAction('subscribe')}
className="cursor-pointer"
>
<RefreshCw className="h-4 w-4 mr-2" />
Subscribe to My Events
<div className="text-xs text-var(--purple-lavender) mt-0.5">
Auto-syncs your RSVP'd events
</div>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => handleCalendarAction('all-events')}
className="cursor-pointer"
>
<Download className="h-4 w-4 mr-2" />
Download All Events
<div className="text-xs text-var(--purple-lavender) mt-0.5">
One-time import of all upcoming events
</div>
</DropdownMenuItem>
</>
)}
{!event && !showSubscribe && (
<div className="px-2 py-6 text-center text-sm text-var(--purple-lavender)">
No event selected
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}