import { appPath } from "@agent-native/core/client/api-path"; import { DevDatabaseLink } from "@agent-native/core/client/db-admin"; import { LanguagePicker, useT } from "@agent-native/core/client/i18n"; import { openCommandMenu } from "@agent-native/core/client/navigation"; import { OrgSwitcher } from "@agent-native/core/client/org"; import { FeedbackButton } from "@agent-native/core/client/ui"; import { SidebarFooterActions } from "@agent-native/toolkit/app-shell"; import { IconCalendar, IconSettings, IconLink, IconExternalLink, IconChevronUp, IconChevronDown, IconChevronLeft, IconChevronRight, IconPlus, IconX, IconInfoCircle, IconCheck, IconEye, IconEyeOff, IconLayoutSidebarLeftCollapse, IconLayoutSidebarLeftExpand, IconSearch, } from "@tabler/icons-react"; import { startOfMonth, endOfMonth, startOfWeek, endOfWeek, addDays, addMonths, subMonths, isSameMonth, isSameDay, isToday, format, setMonth, setYear, getMonth, getYear, } from "date-fns"; import { useState, useEffect, useMemo } from "react"; import { Link, useLocation, useNavigate } from "react-router"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useExternalCalendars, useRemoveExternalCalendar, useUpdateExternalCalendarColor, } from "@/hooks/use-external-calendars"; import { useGoogleAuthStatus, useGoogleAuthUrl, useGoogleAddAccountUrl, useGoogleDesktopAuth, } from "@/hooks/use-google-auth"; import { useOverlayPeople, useRemoveOverlayPerson, useUpdateOverlayPersonColor, } from "@/hooks/use-overlay-people"; import { useViewPreferences } from "@/hooks/use-view-preferences"; import { CALENDAR_COLORS, defaultColorForAccount, type CalendarColorMode, } from "@/lib/calendar-view-preferences"; import { EVENT_CATEGORY_COLORS } from "@/lib/event-colors"; import { cn } from "@/lib/utils"; import { useCalendarContext } from "./AppLayout"; const navItems = [ { path: "/", labelKey: "navigation.calendar", icon: IconCalendar }, { path: "/booking-links", labelKey: "navigation.bookingLinks", icon: IconLink, }, ]; const bottomNavItems = [ { path: "/settings", labelKey: "navigation.settings", icon: IconSettings }, ]; interface SidebarProps { open: boolean; onClose: () => void; collapsed?: boolean; onCollapsedChange?: (collapsed: boolean) => void; } const MONTH_LABELS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; function MonthYearPicker({ viewMonth, onPick, onClose, }: { viewMonth: Date; onPick: (date: Date) => void; onClose: () => void; }) { const [year, setYearState] = useState(() => getYear(viewMonth)); const today = new Date(); const todayMonth = getMonth(today); const todayYear = getYear(today); const viewedMonthIdx = getMonth(viewMonth); const viewedYear = getYear(viewMonth); const t = useT(); return (
{year}
{MONTH_LABELS.map((label, idx) => { const isCurrent = idx === todayMonth && year === todayYear; const isSelected = idx === viewedMonthIdx && year === viewedYear; return ( ); })}
); } function MiniCalendar({ selectedDate, onDateSelect, }: { selectedDate: Date; onDateSelect: (date: Date) => void; }) { const [viewMonth, setViewMonth] = useState(() => startOfMonth(selectedDate)); const [pickerOpen, setPickerOpen] = useState(false); // Sync viewMonth when selectedDate changes to a different month useEffect(() => { if (!isSameMonth(viewMonth, selectedDate)) { setViewMonth(startOfMonth(selectedDate)); } }, [selectedDate]); const days = useMemo(() => { const monthStart = startOfMonth(viewMonth); const monthEnd = endOfMonth(viewMonth); const calStart = startOfWeek(monthStart); const calEnd = endOfWeek(monthEnd); const result: Date[] = []; let current = calStart; while (current <= calEnd) { result.push(current); current = addDays(current, 1); } return result; }, [viewMonth]); const weekdays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; return (
{/* Month header with navigation */}
setViewMonth(startOfMonth(d))} onClose={() => setPickerOpen(false)} />
{/* Weekday headers */}
{weekdays.map((d) => (
{d}
))}
{/* Date grid */}
{days.map((day) => { const inMonth = isSameMonth(day, viewMonth); const today = isToday(day); const selected = isSameDay(day, selectedDate); return ( ); })}
); } function GoogleConnectSidebarButton() { const t = useT(); const [wantAuthUrl, setWantAuthUrl] = useState(false); const authUrl = useGoogleAuthUrl(wantAuthUrl); const { isDesktopGoogleAuth, isGoogleDesktopAuthPending, startDesktopGoogleAuth, } = useGoogleDesktopAuth({ onError: (issue) => toast.error(issue.message || issue.error || t("settings.googleFailed")), onSuccess: () => window.location.reload(), }); useEffect(() => { if (!wantAuthUrl || !authUrl.data?.url) return; setWantAuthUrl(false); window.open(authUrl.data.url, "_blank"); }, [wantAuthUrl, authUrl.data]); useEffect(() => { if (!authUrl.error) return; toast.error(authUrl.error.message); setWantAuthUrl(false); }, [authUrl.error]); function handleConnect() { if (isDesktopGoogleAuth) { startDesktopGoogleAuth({ previousAccountCount: 0 }); return; } setWantAuthUrl(true); } return (

{t("settings.connectGoogleCalendar")}

{t("settings.connectGoogleDescription")}

); } /** A conic-gradient dot indicating "multiple colors" (by-type mode) */ function MultiColorDot({ className }: { className?: string }) { const colors = Object.values(EVENT_CATEGORY_COLORS).filter((_, i) => i < 4); const pct = 100 / colors.length; const stops = colors .map((c, i) => `${c} ${i * pct}% ${(i + 1) * pct}%`) .join(", "); return ( ); } /** Popover color picker for a single-color selection */ function ColorPickerPopover({ color, onColorChange, children, }: { color: string; onColorChange: (color: string) => void; children: React.ReactNode; }) { return ( {children}
{CALENDAR_COLORS.map((c) => ( ))}
); } function GoogleAccountsSection({ accounts, onClose, }: { accounts: Array<{ email: string }>; onClose: () => void; }) { const t = useT(); const { toggleHiddenCalendar, isHiddenCalendar } = useCalendarContext(); const { prefs: { colorMode, singleColor, accountColorModes, accountColors }, updateAccountColor, updateAccountColorMode, } = useViewPreferences(); const accountEmails = useMemo( () => accounts.map((account) => account.email), [accounts], ); const [wantAddAccount, setWantAddAccount] = useState(false); const addAccountUrl = useGoogleAddAccountUrl(wantAddAccount); const { isDesktopGoogleAuth, isGoogleDesktopAuthPending, startDesktopGoogleAuth, } = useGoogleDesktopAuth({ onError: (issue) => toast.error(issue.message || issue.error || t("settings.googleFailed")), onSuccess: () => window.location.reload(), }); useEffect(() => { if (!wantAddAccount || !addAccountUrl.data?.url) return; window.open(addAccountUrl.data.url, "_blank"); setWantAddAccount(false); }, [wantAddAccount, addAccountUrl.data]); useEffect(() => { if (!addAccountUrl.error) return; toast.error(addAccountUrl.error.message); setWantAddAccount(false); }, [addAccountUrl.error]); function handleAddAccount() { if (isDesktopGoogleAuth) { startDesktopGoogleAuth({ addAccount: true, previousAccountCount: accounts.length, }); return; } setWantAddAccount(true); } function accountColorMode(email: string): CalendarColorMode { return accountColorModes[email] ?? colorMode; } function accountColor(email: string): string { return ( accountColors[email] ?? singleColor ?? defaultColorForAccount(email, accountEmails) ); } function handlePickColor(email: string, color: string) { updateAccountColor(email, color); } function handleSetColorMode(email: string, mode: CalendarColorMode) { updateAccountColorMode(email, mode); } return (
{t("sidebar.myCalendars")}
{t("sidebar.googleCalendarSettings")} {t("sidebar.addGoogleAccount")}
{accounts.map((account) => { const mode = accountColorMode(account.email); const color = accountColor(account.email); return (
{/* Multicolor "by type" option */} {t("sidebar.colorByMeetingType")} {/* Single color options */} {CALENDAR_COLORS.map((c) => ( ))}

{account.email}

); })}
); } export function Sidebar({ open, onClose, collapsed = false, onCollapsedChange, }: SidebarProps) { const location = useLocation(); const navigate = useNavigate(); const t = useT(); const { selectedDate, setSelectedDate, setAddCalendarOpen, setAddCalendarDefaultTab, toggleHiddenCalendar, isHiddenCalendar, } = useCalendarContext(); const googleStatus = useGoogleAuthStatus(); const { data: rawOverlayPeople } = useOverlayPeople(); const overlayPeople = Array.isArray(rawOverlayPeople) ? rawOverlayPeople : []; const removePerson = useRemoveOverlayPerson(); const updatePersonColor = useUpdateOverlayPersonColor(); const { data: rawExternalCalendars } = useExternalCalendars(); const externalCalendars = Array.isArray(rawExternalCalendars) ? rawExternalCalendars : []; const removeExternal = useRemoveExternalCalendar(); const updateExternalColor = useUpdateExternalCalendarColor(); const isConnected = googleStatus.data?.connected ?? false; const [peopleGroupOpen, setPeopleGroupOpen] = useState( () => overlayPeople.length <= 2, // i18n-ignore scanner false positive ); const [feedsGroupOpen, setFeedsGroupOpen] = useState( () => externalCalendars.length <= 2, // i18n-ignore scanner false positive ); useEffect(() => { if (overlayPeople.length <= 2) setPeopleGroupOpen(true); }, [overlayPeople.length]); useEffect(() => { if (externalCalendars.length <= 2) setFeedsGroupOpen(true); }, [externalCalendars.length]); function handleMiniCalendarDateSelect(date: Date) { setSelectedDate(date); if (location.pathname !== "/") { navigate("/"); } onClose(); } const collapseButton = onCollapsedChange ? ( {collapsed ? t("sidebar.expandSidebar") : t("sidebar.collapseSidebar")} ) : null; const searchButton = ( {t("root.commandSearch")} ); const translateButton = ( ); const feedbackButton = ( ); return ( <> {/* Mobile overlay */} {open && (
)} ); }