"use client";
import { useState, useRef, useEffect } from 'react';
import { useTranslations } from 'next-intl';
import { useCalendarStore } from '@/stores/calendar-store';
import { useAuthStore } from '@/stores/auth-store';
import { toast } from '@/stores/toast-store';
import { SettingsSection } from './settings-section';
import { Plus, Pencil, Trash2, Check, X, Calendar as CalendarIcon, Copy, Link, Upload, Globe, RefreshCw, Eraser } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ICalImportModal } from '@/components/calendar/ical-import-modal';
import { ICalSubscriptionModal } from '@/components/calendar/ical-subscription-modal';
const CALENDAR_COLORS = [
"#3b82f6", // blue
"#ef4444", // red
"#22c55e", // green
"#f59e0b", // amber
"#8b5cf6", // violet
"#ec4899", // pink
"#14b8a6", // teal
"#f97316", // orange
"#06b6d4", // cyan
"#84cc16", // lime
"#6366f1", // indigo
"#a855f7", // purple
"#e11d48", // rose
"#0ea5e9", // sky
"#10b981", // emerald
"#d946ef", // fuchsia
];
function CalendarColorPicker({
value,
onChange,
allowCustom,
}: {
value: string;
onChange: (color: string) => void;
allowCustom?: boolean;
}) {
const selectedIsPreset = CALENDAR_COLORS.includes(value);
return (
{CALENDAR_COLORS.map((color) => (
);
}
function CalendarEditForm({
initial,
onSave,
onCancel,
isLoading,
}: {
initial?: { name: string; color: string };
onSave: (data: { name: string; color: string }) => void;
onCancel: () => void;
isLoading: boolean;
}) {
const t = useTranslations('calendar.management');
const [name, setName] = useState(initial?.name || '');
const [color, setColor] = useState(initial?.color || '#3b82f6');
const isValid = name.trim().length > 0;
return (
setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && isValid) onSave({ name: name.trim(), color });
if (e.key === 'Escape') onCancel();
}}
placeholder={t('name_placeholder')}
className="w-full px-3 py-1.5 text-sm rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
autoFocus
disabled={isLoading}
/>
);
}
export { CalendarColorPicker, CALENDAR_COLORS };
export function CalendarManagementSettings() {
const t = useTranslations('calendar.management');
const { client, serverUrl, username } = useAuthStore();
const { calendars, updateCalendar, createCalendar, removeCalendar, clearCalendarEvents, fetchCalendars, icalSubscriptions, removeICalSubscription, refreshICalSubscription, isSubscriptionCalendar } = useCalendarStore();
const [isCreating, setIsCreating] = useState(false);
const [editingId, setEditingId] = useState(null);
const [deletingId, setDeletingId] = useState(null);
const [clearingId, setClearingId] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [colorPickerId, setColorPickerId] = useState(null);
const [showImportModal, setShowImportModal] = useState(false);
const [showSubscriptionModal, setShowSubscriptionModal] = useState(false);
const [deletingSubId, setDeletingSubId] = useState(null);
const [refreshingSubId, setRefreshingSubId] = useState(null);
const tImport = useTranslations('calendar.import');
const tSub = useTranslations('calendar.subscription');
const colorPickerRef = useRef(null);
// Load calendars if not yet loaded
useEffect(() => {
if (client && calendars.length === 0) {
fetchCalendars(client);
}
}, [client, calendars.length, fetchCalendars]);
const handleRefreshSubscription = async (subId: string) => {
if (!client) return;
setRefreshingSubId(subId);
try {
await refreshICalSubscription(client, subId);
toast.success(tSub('refresh_success'));
} catch {
toast.error(tSub('refresh_error'));
} finally {
setRefreshingSubId(null);
}
};
const handleDeleteSubscription = async (subId: string) => {
if (!client) return;
setIsLoading(true);
try {
await removeICalSubscription(client, subId);
setDeletingSubId(null);
toast.success(tSub('deleted'));
} catch {
toast.error(tSub('delete_error'));
} finally {
setIsLoading(false);
}
};
// Close color picker on click outside
useEffect(() => {
if (!colorPickerId) return;
const handleClick = (e: MouseEvent) => {
if (colorPickerRef.current && !colorPickerRef.current.contains(e.target as Node)) {
setColorPickerId(null);
}
};
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setColorPickerId(null);
};
document.addEventListener('mousedown', handleClick);
document.addEventListener('keydown', handleKey);
return () => {
document.removeEventListener('mousedown', handleClick);
document.removeEventListener('keydown', handleKey);
};
}, [colorPickerId]);
const handleCreate = async (data: { name: string; color: string }) => {
if (!client) return;
setIsLoading(true);
try {
await createCalendar(client, {
name: data.name,
color: data.color,
isVisible: true,
isSubscribed: true,
});
setIsCreating(false);
toast.success(t('calendar_created'));
} catch {
toast.error(t('error_create'));
} finally {
setIsLoading(false);
}
};
const handleUpdate = async (calendarId: string, data: { name: string; color: string }) => {
if (!client) return;
setIsLoading(true);
try {
await updateCalendar(client, calendarId, { name: data.name, color: data.color });
setEditingId(null);
toast.success(t('calendar_updated'));
} catch {
toast.error(t('error_update'));
} finally {
setIsLoading(false);
}
};
const handleColorChange = async (calendarId: string, color: string) => {
if (!client) return;
try {
await updateCalendar(client, calendarId, { color });
toast.success(t('color_updated'));
} catch {
toast.error(t('error_update'));
}
setColorPickerId(null);
};
const handleDelete = async (calendarId: string) => {
if (!client) return;
setIsLoading(true);
try {
await removeCalendar(client, calendarId);
setDeletingId(null);
toast.success(t('calendar_deleted'));
} catch {
toast.error(t('error_delete'));
} finally {
setIsLoading(false);
}
};
const handleClear = async (calendarId: string) => {
if (!client) return;
setIsLoading(true);
try {
const count = await clearCalendarEvents(client, calendarId);
setClearingId(null);
toast.success(t('events_cleared', { count }));
} catch {
toast.error(t('error_clear'));
} finally {
setIsLoading(false);
}
};
const buildCalDavUrl = (calendarId: string) => {
if (!serverUrl || !username) return null;
const base = serverUrl.replace(/\/$/, '');
return `${base}/dav/calendars/user/${encodeURIComponent(username)}/${encodeURIComponent(calendarId)}/`;
};
const handleCopyUrl = async (url: string) => {
try {
await navigator.clipboard.writeText(url);
toast.success(t('url_copied'));
} catch {
// Fallback for non-HTTPS contexts
const textArea = document.createElement('textarea');
textArea.value = url;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
toast.success(t('url_copied'));
}
};
return (
{calendars.filter(cal => !isSubscriptionCalendar(cal.id)).map((cal) => {
const color = cal.color || '#3b82f6';
if (editingId === cal.id) {
return (
handleUpdate(cal.id, data)}
onCancel={() => setEditingId(null)}
isLoading={isLoading}
/>
);
}
if (deletingId === cal.id) {
return (
{t('confirm_delete', { name: cal.name })}
);
}
if (clearingId === cal.id) {
return (
{t('confirm_clear', { name: cal.name })}
);
}
return (
{/* Color swatch - clickable to change color */}
{cal.name}
{(() => {
const caldavUrl = buildCalDavUrl(cal.id);
if (!caldavUrl) return null;
return (
{caldavUrl}
);
})()}
{cal.isDefault && (
{t('default')}
)}
{!cal.isDefault && (
)}
);
})}
{isCreating ? (
setIsCreating(false)}
isLoading={isLoading}
/>
) : (
)}
{/* iCal Subscriptions */}
{icalSubscriptions.length > 0 && (
{tSub('section_title')}
{icalSubscriptions.map((sub) => {
if (deletingSubId === sub.id) {
return (
{tSub('confirm_delete', { name: sub.name })}
);
}
return (
{sub.name}
{sub.url}
{sub.lastRefreshed && (
{tSub('last_refreshed', { time: new Date(sub.lastRefreshed).toLocaleString() })}
)}
);
})}
)}
{showImportModal && client && (
setShowImportModal(false)}
/>
)}
{showSubscriptionModal && client && (
setShowSubscriptionModal(false)}
/>
)}
);
}