- 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.
217 lines
7.5 KiB
JavaScript
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>
|
|
);
|
|
}
|