Update New Features
This commit is contained in:
216
src/components/AddToCalendarButton.js
Normal file
216
src/components/AddToCalendarButton.js
Normal file
@@ -0,0 +1,216 @@
|
||||
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-[#422268]">
|
||||
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-[#422268]">
|
||||
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-[#664fa3] 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-[#664fa3] 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-[#664fa3]">
|
||||
No event selected
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user