import React, { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { CalendarDays, Copy, Eye, KeyRound, RefreshCw, ShieldOff, Settings2 } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; function PlatformNote({ title, children }) { return (

{title}

{children}

); } export function CalendarFeedManager({ compact = false, showManageLink = false }) { const [feed, setFeed] = useState(null); const [preview, setPreview] = useState([]); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(null); const loadFeed = useCallback(async () => { setLoading(true); try { const data = await api.calendarFeed(); setFeed(data); if (data?.active) { const nextPreview = await api.calendarFeedPreview(10); setPreview(nextPreview.events || []); } else { setPreview([]); } } catch (err) { toast.error(err.message || 'Failed to load calendar feed.'); } finally { setLoading(false); } }, []); useEffect(() => { loadFeed(); }, [loadFeed]); const active = !!feed?.active && !!feed?.feed_url; async function createFeed() { setBusy('create'); try { const data = await api.createCalendarFeed(); setFeed(data); const nextPreview = await api.calendarFeedPreview(10); setPreview(nextPreview.events || []); toast.success('Calendar feed created.'); } catch (err) { toast.error(err.message || 'Failed to create calendar feed.'); } finally { setBusy(null); } } async function copyFeedUrl() { if (!feed?.feed_url) return; try { await navigator.clipboard.writeText(feed.feed_url); toast.success('Calendar feed URL copied.'); } catch { toast.error('Copy failed. Select the URL and copy it manually.'); } } async function regenerateFeed() { setBusy('regenerate'); try { const data = await api.regenerateCalendarFeed(); setFeed(data); const nextPreview = await api.calendarFeedPreview(10); setPreview(nextPreview.events || []); toast.success('Calendar feed regenerated. Update any subscribed calendars with the new URL.'); } catch (err) { toast.error(err.message || 'Failed to regenerate calendar feed.'); } finally { setBusy(null); } } async function revokeFeed() { setBusy('revoke'); try { const data = await api.revokeCalendarFeed(); setFeed(data); setPreview([]); toast.success('Calendar feed revoked.'); } catch (err) { toast.error(err.message || 'Failed to revoke calendar feed.'); } finally { setBusy(null); } } return (

Subscribe from Apple Calendar, Google Calendar, Android, Outlook, or any ICS calendar.

This creates a private calendar feed URL. Nothing is added automatically; copy the URL into your calendar app to subscribe.

{!loading && !active && ( )}
{loading && (
)} {!loading && !active && (

What happens next?

1. Create

Generate a private feed URL for your bill calendar.

2. Copy

Paste it into Apple, Google, Outlook, or Android calendar setup.

3. Subscribe

Your calendar app refreshes bill due dates when it checks the feed.

)} {!loading && active && ( <>
Anyone with this URL can see the bill events in this feed. Regenerate or revoke it if it was shared somewhere it should not be.
Add a calendar subscription with the copied URL. The feed uses all-day dates to avoid timezone shifts. In Google Calendar on the web, use Other calendars, From URL. Android follows Google Calendar sync. Subscribe from Outlook on the web with this URL. Imported copies will not update; subscriptions will. Bill Tracker emits stable event IDs per bill cycle so subscribed calendars can update without double-adding events.

Next events that will appear

Last fetched: {feed.last_used_at ? new Date(feed.last_used_at).toLocaleString() : 'Not yet'}
{preview.length === 0 && (

No upcoming bill events in the preview window.

)} {preview.map(event => (

{event.name}

{event.due_date} ยท {event.cycle_type}

${Number(event.amount || 0).toFixed(2)}
))}
{showManageLink && ( )}
)}
); }