Compare commits
No commits in common. "c5a309f4a1969fb7f112baca2c89d4110c22bc80" and "1daec977bab35fa4cef22d417909429312119c41" have entirely different histories.
c5a309f4a1
...
1daec977ba
@ -51,10 +51,7 @@ export default function CalendarPage() {
|
||||
const [createDefaults, setCreateDefaults] = useState<CreateDefaults | null>(null);
|
||||
|
||||
const { settings } = useSettings();
|
||||
const firstDayOfWeek = settings?.first_day_of_week ?? 0;
|
||||
const { data: calendars = [], sharedData, allCalendarIds } = useCalendars({ pollingEnabled: true });
|
||||
const [currentDate, setCurrentDate] = useState<string>(() => format(new Date(), 'yyyy-MM-dd'));
|
||||
const [navKey, setNavKey] = useState(0);
|
||||
const [visibleSharedIds, setVisibleSharedIds] = useState<Set<number>>(new Set());
|
||||
const calendarContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -110,10 +107,6 @@ export default function CalendarPage() {
|
||||
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarWidth));
|
||||
}, [sidebarWidth]);
|
||||
|
||||
const handleMiniCalClick = useCallback((dateStr: string) => {
|
||||
calendarRef.current?.getApi().gotoDate(dateStr);
|
||||
}, []);
|
||||
|
||||
// Location data for event panel
|
||||
const { data: locations = [] } = useQuery({
|
||||
queryKey: ['locations'],
|
||||
@ -520,10 +513,6 @@ export default function CalendarPage() {
|
||||
setVisibleRange((prev) =>
|
||||
prev.start === start && prev.end === end ? prev : { start, end }
|
||||
);
|
||||
// Track current date anchor for mini calendar sync
|
||||
setCurrentDate(format(arg.view.currentStart, 'yyyy-MM-dd'));
|
||||
// Increment nav key so mini calendar clears selection even when month doesn't change
|
||||
setNavKey((k) => k + 1);
|
||||
};
|
||||
|
||||
const navigatePrev = () => calendarRef.current?.getApi().prev();
|
||||
@ -602,7 +591,7 @@ export default function CalendarPage() {
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden animate-fade-in">
|
||||
<div className="hidden lg:flex lg:flex-row shrink-0">
|
||||
<CalendarSidebar ref={sidebarRef} onUseTemplate={handleUseTemplate} onSharedVisibilityChange={setVisibleSharedIds} width={sidebarWidth} onDateClick={handleMiniCalClick} currentDate={currentDate} firstDayOfWeek={firstDayOfWeek} navKey={navKey} />
|
||||
<CalendarSidebar ref={sidebarRef} onUseTemplate={handleUseTemplate} onSharedVisibilityChange={setVisibleSharedIds} width={sidebarWidth} />
|
||||
<div
|
||||
onMouseDown={handleSidebarMouseDown}
|
||||
className="w-1 shrink-0 cursor-col-resize hover:bg-accent/30 active:bg-accent/50 transition-colors duration-150"
|
||||
@ -613,7 +602,7 @@ export default function CalendarPage() {
|
||||
<Sheet open={mobileSidebarOpen} onOpenChange={setMobileSidebarOpen}>
|
||||
<SheetContent className="w-72 p-0">
|
||||
<SheetClose onClick={() => setMobileSidebarOpen(false)} />
|
||||
<CalendarSidebar onUseTemplate={(tmpl) => { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} onDateClick={(dateStr) => { setMobileSidebarOpen(false); handleMiniCalClick(dateStr); }} currentDate={currentDate} firstDayOfWeek={firstDayOfWeek} navKey={navKey} />
|
||||
<CalendarSidebar onUseTemplate={(tmpl) => { setMobileSidebarOpen(false); handleUseTemplate(tmpl); }} onSharedVisibilityChange={setVisibleSharedIds} width={288} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
@ -723,12 +712,12 @@ export default function CalendarPage() {
|
||||
>
|
||||
<div className="h-full">
|
||||
<FullCalendar
|
||||
key={`fc-${firstDayOfWeek}`}
|
||||
key={`fc-${settings?.first_day_of_week ?? 0}`}
|
||||
ref={calendarRef}
|
||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||
initialView="dayGridMonth"
|
||||
headerToolbar={false}
|
||||
firstDay={firstDayOfWeek}
|
||||
firstDay={settings?.first_day_of_week ?? 0}
|
||||
events={calendarEvents}
|
||||
editable={true}
|
||||
selectable={true}
|
||||
|
||||
@ -10,19 +10,14 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
import CalendarForm from './CalendarForm';
|
||||
import TemplateForm from './TemplateForm';
|
||||
import SharedCalendarSection, { loadVisibility, saveVisibility } from './SharedCalendarSection';
|
||||
import MiniCalendar from './MiniCalendar';
|
||||
|
||||
interface CalendarSidebarProps {
|
||||
onUseTemplate?: (template: EventTemplate) => void;
|
||||
onSharedVisibilityChange?: (visibleIds: Set<number>) => void;
|
||||
width: number;
|
||||
onDateClick?: (dateStr: string) => void;
|
||||
currentDate?: string;
|
||||
firstDayOfWeek?: number;
|
||||
navKey?: number;
|
||||
}
|
||||
|
||||
const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(function CalendarSidebar({ onUseTemplate, onSharedVisibilityChange, width, onDateClick, currentDate, firstDayOfWeek, navKey }, ref) {
|
||||
const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(function CalendarSidebar({ onUseTemplate, onSharedVisibilityChange, width }, ref) {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: calendars = [], sharedData: sharedCalendars = [] } = useCalendars();
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
@ -100,35 +95,19 @@ const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(functio
|
||||
|
||||
return (
|
||||
<div ref={ref} className="shrink-0 border-r bg-card flex flex-col" style={{ width }}>
|
||||
<div className="h-16 px-4 border-b flex items-center shrink-0">
|
||||
<div className="h-16 px-4 border-b flex items-center justify-between shrink-0">
|
||||
<span className="text-sm font-semibold font-heading text-foreground">Calendars</span>
|
||||
</div>
|
||||
{onDateClick && (
|
||||
<div className="border-b shrink-0">
|
||||
<MiniCalendar
|
||||
onDateClick={onDateClick}
|
||||
currentDate={currentDate}
|
||||
firstDayOfWeek={firstDayOfWeek}
|
||||
navKey={navKey}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
||||
{/* Owned calendars list (non-shared only) */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
My Calendars
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-5 w-5"
|
||||
className="h-7 w-7"
|
||||
onClick={() => { setEditingCalendar(null); setShowForm(true); }}
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-4">
|
||||
{/* Owned calendars list (non-shared only) */}
|
||||
<div className="space-y-0.5">
|
||||
{calendars.filter((c) => !c.is_shared).map((cal) => (
|
||||
<div
|
||||
@ -159,7 +138,6 @@ const CalendarSidebar = forwardRef<HTMLDivElement, CalendarSidebarProps>(functio
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shared calendars section -- owned + member */}
|
||||
{(calendars.some((c) => c.is_shared) || sharedCalendars.length > 0) && (
|
||||
|
||||
@ -1,166 +0,0 @@
|
||||
import { useState, useEffect, useMemo, useCallback, memo } from 'react';
|
||||
import {
|
||||
startOfMonth, endOfMonth, startOfWeek, endOfWeek,
|
||||
eachDayOfInterval, format, isSameDay, isSameMonth, isToday,
|
||||
addMonths, subMonths, parse,
|
||||
} from 'date-fns';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface MiniCalendarProps {
|
||||
onDateClick: (dateStr: string) => void;
|
||||
currentDate?: string;
|
||||
firstDayOfWeek?: number;
|
||||
navKey?: number;
|
||||
}
|
||||
|
||||
const DAY_LABELS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||
|
||||
function buildGrid(displayed: Date, firstDay: number) {
|
||||
const monthStart = startOfMonth(displayed);
|
||||
const monthEnd = endOfMonth(displayed);
|
||||
const gridStart = startOfWeek(monthStart, { weekStartsOn: firstDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 });
|
||||
const gridEnd = endOfWeek(monthEnd, { weekStartsOn: firstDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 });
|
||||
return eachDayOfInterval({ start: gridStart, end: gridEnd });
|
||||
}
|
||||
|
||||
function getOrderedLabels(firstDay: number) {
|
||||
return [...DAY_LABELS.slice(firstDay), ...DAY_LABELS.slice(0, firstDay)];
|
||||
}
|
||||
|
||||
const MiniCalendar = memo(function MiniCalendar({
|
||||
onDateClick,
|
||||
currentDate,
|
||||
firstDayOfWeek = 0,
|
||||
navKey,
|
||||
}: MiniCalendarProps) {
|
||||
const REF_DATE = useMemo(() => new Date(), []);
|
||||
|
||||
const parseDate = useCallback(
|
||||
(dateStr: string) => parse(dateStr, 'yyyy-MM-dd', REF_DATE),
|
||||
[REF_DATE]
|
||||
);
|
||||
|
||||
const [displayedMonth, setDisplayedMonth] = useState(() =>
|
||||
currentDate ? startOfMonth(parseDate(currentDate)) : startOfMonth(new Date())
|
||||
);
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null);
|
||||
|
||||
// Sync displayed month when main calendar navigates across months
|
||||
useEffect(() => {
|
||||
if (!currentDate) return;
|
||||
const incoming = startOfMonth(parseDate(currentDate));
|
||||
setDisplayedMonth((prev) =>
|
||||
prev.getTime() === incoming.getTime() ? prev : incoming
|
||||
);
|
||||
}, [currentDate, parseDate]);
|
||||
|
||||
// Clear selection on any toolbar navigation (today/prev/next)
|
||||
// navKey increments on every datesSet, even when the month doesn't change
|
||||
useEffect(() => {
|
||||
setSelectedDate(null);
|
||||
}, [navKey]);
|
||||
|
||||
const days = useMemo(
|
||||
() => buildGrid(displayedMonth, firstDayOfWeek),
|
||||
[displayedMonth, firstDayOfWeek]
|
||||
);
|
||||
|
||||
const orderedLabels = useMemo(
|
||||
() => getOrderedLabels(firstDayOfWeek),
|
||||
[firstDayOfWeek]
|
||||
);
|
||||
|
||||
const handlePrev = useCallback(() => setDisplayedMonth((m) => subMonths(m, 1)), []);
|
||||
const handleNext = useCallback(() => setDisplayedMonth((m) => addMonths(m, 1)), []);
|
||||
|
||||
const handleDayClick = useCallback((day: Date) => {
|
||||
const dateStr = format(day, 'yyyy-MM-dd');
|
||||
setSelectedDate(dateStr);
|
||||
setDisplayedMonth((prev) => isSameMonth(day, prev) ? prev : startOfMonth(day));
|
||||
onDateClick(dateStr);
|
||||
}, [onDateClick]);
|
||||
|
||||
const selectedDateObj = useMemo(
|
||||
() => selectedDate ? parseDate(selectedDate) : null,
|
||||
[selectedDate, parseDate]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="px-3 pt-3 pb-2 max-w-[280px] mx-auto">
|
||||
{/* Month header with navigation */}
|
||||
<div className="flex items-center justify-between mb-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={handlePrev}
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<span className="text-sm font-medium font-heading select-none">
|
||||
{format(displayedMonth, 'MMMM yyyy')}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Day-of-week headers */}
|
||||
<div className="grid grid-cols-7 mb-0.5">
|
||||
{orderedLabels.map((label) => (
|
||||
<span
|
||||
key={label}
|
||||
className="text-[10px] text-muted-foreground text-center select-none"
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Day grid */}
|
||||
<div className="grid grid-cols-7">
|
||||
{days.map((day) => {
|
||||
const isCurrentMonth = isSameMonth(day, displayedMonth);
|
||||
const today = isToday(day);
|
||||
const isSelected = selectedDateObj ? isSameDay(day, selectedDateObj) : false;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={format(day, 'yyyy-MM-dd')}
|
||||
type="button"
|
||||
aria-label={format(day, 'EEEE, MMMM d, yyyy')}
|
||||
onClick={() => handleDayClick(day)}
|
||||
className={[
|
||||
'h-7 text-xs flex items-center justify-center rounded-md transition-colors duration-100',
|
||||
isSelected
|
||||
? 'font-medium'
|
||||
: today
|
||||
? 'font-semibold'
|
||||
: isCurrentMonth
|
||||
? 'text-foreground hover:bg-accent/10'
|
||||
: 'text-muted-foreground/40 hover:bg-accent/10',
|
||||
].join(' ')}
|
||||
style={
|
||||
isSelected
|
||||
? { backgroundColor: 'hsl(var(--accent-color))', color: 'hsl(var(--accent-foreground))' }
|
||||
: today && !isSelected
|
||||
? { backgroundColor: 'hsl(var(--accent-color) / 0.2)', color: 'hsl(var(--accent-color))' }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{format(day, 'd')}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default MiniCalendar;
|
||||
Loading…
x
Reference in New Issue
Block a user