✨(front) add main Scheduler component
Add Scheduler component integrating FullCalendar for event display with day/week/month views, drag-drop support and CalDAV synchronization via CalendarContext. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
.day-of-month {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.my-event {
|
||||
// opacity: 0.5;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Event Modal Styles (for use with Cunningham Modal)
|
||||
// ============================================================================
|
||||
|
||||
.event-modal {
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
// Date/Time row with two columns
|
||||
&__datetime-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Features section (buttons + inputs)
|
||||
&__features {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
&__attendees-input {
|
||||
padding: 0.75rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
&__recurrence-editor {
|
||||
padding: 0.75rem;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
// Checkbox for all-day
|
||||
&__checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
}
|
||||
}
|
||||
|
||||
// Feature tag button
|
||||
&__feature-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 1rem;
|
||||
background-color: #fff;
|
||||
color: #495057;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background-color: #e7f5ff;
|
||||
border-color: #339af0;
|
||||
color: #1971c2;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Invitation section
|
||||
&__invitation {
|
||||
padding: 1rem;
|
||||
background-color: #e7f5ff;
|
||||
border: 1px solid #339af0;
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&__invitation-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
&__invitation-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #1971c2;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
&__invitation-organizer {
|
||||
font-size: 0.9375rem;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
&__invitation-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__invitation-status {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #fff;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
color: #495057;
|
||||
|
||||
strong {
|
||||
color: #1971c2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Delete Event Modal Styles
|
||||
// ============================================================================
|
||||
|
||||
.delete-modal {
|
||||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
&__message {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
&__options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
&__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #f8f9fa;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
cursor: pointer;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin: 0;
|
||||
accent-color: #dc3545; // Error color for delete action
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
font-size: 0.9375rem;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
input[type="radio"]:checked + span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Scheduler component using EventCalendar (vkurko/calendar).
|
||||
* Renders a CalDAV-connected calendar view with full interactivity.
|
||||
*
|
||||
* Features:
|
||||
* - Drag & drop events (eventDrop)
|
||||
* - Resize events (eventResize)
|
||||
* - Click to edit (eventClick)
|
||||
* - Click to create (dateClick)
|
||||
* - Select range to create (select)
|
||||
*
|
||||
* Next.js consideration: This component must be client-side only
|
||||
* due to DOM manipulation. Use dynamic import with ssr: false if needed.
|
||||
*/
|
||||
|
||||
import "@event-calendar/core/index.css";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useCalendarContext } from "../../contexts/CalendarContext";
|
||||
import type { CalDavCalendar } from "../../services/dav/types/caldav-service";
|
||||
import type { EventCalendarEvent } from "../../services/dav/types/event-calendar";
|
||||
|
||||
import { EventModal } from "./EventModal";
|
||||
import type { SchedulerProps, EventModalState } from "./types";
|
||||
import { useSchedulerHandlers } from "./hooks/useSchedulerHandlers";
|
||||
import {
|
||||
useSchedulerInit,
|
||||
useSchedulingCapabilitiesCheck,
|
||||
} from "./hooks/useSchedulerInit";
|
||||
|
||||
type ECEvent = EventCalendarEvent;
|
||||
|
||||
// Calendar API interface
|
||||
interface CalendarApi {
|
||||
updateEvent: (event: ECEvent) => void;
|
||||
addEvent: (event: ECEvent) => void;
|
||||
unselect: () => void;
|
||||
refetchEvents: () => void;
|
||||
$destroy?: () => void;
|
||||
}
|
||||
|
||||
export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
||||
const {
|
||||
caldavService,
|
||||
adapter,
|
||||
davCalendars,
|
||||
visibleCalendarUrls,
|
||||
isConnected,
|
||||
calendarRef: contextCalendarRef,
|
||||
setCurrentDate,
|
||||
} = useCalendarContext();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const calendarRef = contextCalendarRef as React.MutableRefObject<CalendarApi | null>;
|
||||
const [calendarUrl, setCalendarUrl] = useState(defaultCalendarUrl || "");
|
||||
|
||||
// Modal state
|
||||
const [modalState, setModalState] = useState<EventModalState>({
|
||||
isOpen: false,
|
||||
mode: "create",
|
||||
event: null,
|
||||
calendarUrl: "",
|
||||
});
|
||||
|
||||
// Keep refs to visibleCalendarUrls and davCalendars for use in eventFilter/eventSources
|
||||
const visibleCalendarUrlsRef = useRef(visibleCalendarUrls);
|
||||
visibleCalendarUrlsRef.current = visibleCalendarUrls;
|
||||
|
||||
const davCalendarsRef = useRef<CalDavCalendar[]>(davCalendars);
|
||||
davCalendarsRef.current = davCalendars;
|
||||
|
||||
// Initialize calendar URL from context
|
||||
useEffect(() => {
|
||||
if (davCalendars.length > 0 && !calendarUrl) {
|
||||
const firstCalendar = davCalendars[0];
|
||||
setCalendarUrl(defaultCalendarUrl || firstCalendar.url);
|
||||
}
|
||||
}, [davCalendars, defaultCalendarUrl, calendarUrl]);
|
||||
|
||||
// Check scheduling capabilities on mount
|
||||
useSchedulingCapabilitiesCheck(isConnected, caldavService);
|
||||
|
||||
// Initialize event handlers
|
||||
const {
|
||||
handleEventDrop,
|
||||
handleEventResize,
|
||||
handleEventClick,
|
||||
handleDateClick,
|
||||
handleSelect,
|
||||
handleModalSave,
|
||||
handleModalDelete,
|
||||
handleModalClose,
|
||||
handleRespondToInvitation,
|
||||
} = useSchedulerHandlers({
|
||||
adapter,
|
||||
caldavService,
|
||||
davCalendarsRef,
|
||||
calendarRef,
|
||||
calendarUrl,
|
||||
modalState,
|
||||
setModalState,
|
||||
});
|
||||
|
||||
// Initialize calendar
|
||||
// Cast handlers to bypass library type differences between specific event types and unknown
|
||||
useSchedulerInit({
|
||||
containerRef,
|
||||
calendarRef,
|
||||
isConnected,
|
||||
calendarUrl,
|
||||
caldavService,
|
||||
adapter,
|
||||
visibleCalendarUrlsRef,
|
||||
davCalendarsRef,
|
||||
setCurrentDate,
|
||||
handleEventClick: handleEventClick as (info: unknown) => void,
|
||||
handleEventDrop: handleEventDrop as unknown as (info: unknown) => void,
|
||||
handleEventResize: handleEventResize as unknown as (info: unknown) => void,
|
||||
handleDateClick: handleDateClick as (info: unknown) => void,
|
||||
handleSelect: handleSelect as (info: unknown) => void,
|
||||
});
|
||||
|
||||
// Update eventFilter when visible calendars change
|
||||
useEffect(() => {
|
||||
if (calendarRef.current) {
|
||||
// The refs are already updated (synchronously during render)
|
||||
// Now trigger a re-evaluation of eventFilter by calling refetchEvents
|
||||
calendarRef.current.refetchEvents();
|
||||
}
|
||||
}, [visibleCalendarUrls, davCalendars, calendarRef]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={containerRef}
|
||||
id="event-calendar"
|
||||
style={{ height: "calc(100vh - 100px)" }}
|
||||
/>
|
||||
|
||||
<EventModal
|
||||
isOpen={modalState.isOpen}
|
||||
mode={modalState.mode}
|
||||
event={modalState.event}
|
||||
calendarUrl={modalState.calendarUrl}
|
||||
calendars={davCalendars}
|
||||
adapter={adapter}
|
||||
onSave={handleModalSave}
|
||||
onDelete={modalState.mode === "edit" ? handleModalDelete : undefined}
|
||||
onRespondToInvitation={handleRespondToInvitation}
|
||||
onClose={handleModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Scheduler;
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Scheduler components exports.
|
||||
*/
|
||||
|
||||
export { Scheduler } from "./Scheduler";
|
||||
export { EventModal } from "./EventModal";
|
||||
export { DeleteEventModal } from "./DeleteEventModal";
|
||||
export { useSchedulerHandlers } from "./hooks/useSchedulerHandlers";
|
||||
export { useSchedulerInit, useSchedulingCapabilitiesCheck } from "./hooks/useSchedulerInit";
|
||||
export * from "./types";
|
||||
export * from "./utils/dateFormatters";
|
||||
Reference in New Issue
Block a user