♻️(front) reorganize calendar components into subfolders
- Move scheduler components (AttendeesInput, RecurrenceEditor, EventModal styles) to scheduler/ - Move left panel components (LeftPanel, MiniCalendar) to left-panel/ - Move CalendarList.scss to calendar-list/ - Simplify SchedulerToolbar component - Update imports, exports and page layout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,42 +0,0 @@
|
|||||||
/**
|
|
||||||
* LeftPanel component - Calendar sidebar with mini calendar and calendar list.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
|
||||||
|
|
||||||
import { Calendar } from "../api";
|
|
||||||
import { CalendarList } from "./calendar-list";
|
|
||||||
import { MiniCalendar } from "./MiniCalendar";
|
|
||||||
import { useCalendarContext } from "../contexts";
|
|
||||||
|
|
||||||
interface LeftPanelProps {
|
|
||||||
calendars: Calendar[];
|
|
||||||
selectedDate: Date;
|
|
||||||
onDateSelect: (date: Date) => void;
|
|
||||||
onCreateEvent: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LeftPanel = ({
|
|
||||||
calendars,
|
|
||||||
selectedDate,
|
|
||||||
onDateSelect,
|
|
||||||
onCreateEvent,
|
|
||||||
}: LeftPanelProps) => {
|
|
||||||
useCalendarContext();
|
|
||||||
return (
|
|
||||||
<div className="calendar-left-panel">
|
|
||||||
<div className="calendar-left-panel__create">
|
|
||||||
<Button onClick={onCreateEvent} icon={<span className="material-icons">add</span>}>
|
|
||||||
Créer
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MiniCalendar selectedDate={selectedDate} onDateSelect={onDateSelect} />
|
|
||||||
|
|
||||||
<div className="calendar-left-panel__divider" />
|
|
||||||
|
|
||||||
<CalendarList calendars={calendars} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,68 +1,61 @@
|
|||||||
/**
|
/**
|
||||||
* CalendarItemMenu component.
|
* CalendarItemMenu component.
|
||||||
* Context menu for calendar item actions (edit, delete).
|
* Context menu for calendar item actions (edit, delete, subscription).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useRef, useEffect } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { DropdownMenu, DropdownMenuOption } from "@gouvfr-lasuite/ui-kit";
|
||||||
|
|
||||||
import type { CalendarItemMenuProps } from "./types";
|
import type { CalendarItemMenuProps } from "./types";
|
||||||
|
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
export const CalendarItemMenu = ({
|
export const CalendarItemMenu = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onSubscription,
|
onSubscription,
|
||||||
onClose,
|
|
||||||
}: CalendarItemMenuProps) => {
|
}: CalendarItemMenuProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const options: DropdownMenuOption[] = useMemo(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const items: DropdownMenuOption[] = [
|
||||||
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
{
|
||||||
onClose();
|
label: t("calendar.list.edit"),
|
||||||
}
|
icon: <span className="material-icons">edit</span>,
|
||||||
};
|
callback: onEdit,
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
},
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
];
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
onEdit();
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = () => {
|
|
||||||
onDelete();
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubscription = () => {
|
|
||||||
if (onSubscription) {
|
if (onSubscription) {
|
||||||
onSubscription();
|
items.push({
|
||||||
onClose();
|
label: t("calendar.list.subscription"),
|
||||||
|
icon: <span className="material-icons">link</span>,
|
||||||
|
callback: onSubscription,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
items.push({
|
||||||
|
label: t("calendar.list.delete"),
|
||||||
|
icon: <span className="material-icons">delete</span>,
|
||||||
|
callback: onDelete,
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [t, onEdit, onDelete, onSubscription]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menuRef} className="calendar-list__menu">
|
<DropdownMenu options={options} isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<button className="calendar-list__menu-item" onClick={handleEdit}>
|
<Button
|
||||||
<span className="material-icons">edit</span>
|
className="calendar-list__options-btn"
|
||||||
{t('calendar.list.edit')}
|
aria-label={t("calendar.list.options")}
|
||||||
</button>
|
color="brand"
|
||||||
{onSubscription && (
|
variant="tertiary"
|
||||||
<button className="calendar-list__menu-item" onClick={handleSubscription}>
|
size="small"
|
||||||
<span className="material-icons">link</span>
|
onClick={() => onOpenChange(!isOpen)}
|
||||||
{t('calendar.list.subscription')}
|
icon={<span className="material-icons">more_vert</span>}
|
||||||
</button>
|
/>
|
||||||
)}
|
</DropdownMenu>
|
||||||
<button
|
|
||||||
className="calendar-list__menu-item calendar-list__menu-item--danger"
|
|
||||||
onClick={handleDelete}
|
|
||||||
>
|
|
||||||
<span className="material-icons">delete</span>
|
|
||||||
{t('calendar.list.delete')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,7 +57,9 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--c--theme--colors--greyscale-600);
|
color: var(--c--theme--colors--greyscale-600);
|
||||||
transition: background-color 0.15s, color 0.15s;
|
transition:
|
||||||
|
background-color 0.15s,
|
||||||
|
color 0.15s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--c--theme--colors--greyscale-100);
|
background-color: var(--c--theme--colors--greyscale-100);
|
||||||
@@ -72,14 +74,13 @@
|
|||||||
&__items {
|
&__items {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.125rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
padding: 0.375rem 0.5rem;
|
padding: 0.1rem 0.375rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background-color 0.15s;
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
@@ -106,6 +107,12 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
// Override Cunningham checkbox colors with calendar color
|
||||||
|
input:checked {
|
||||||
|
background-color: var(--calendar-color) !important;
|
||||||
|
border-color: var(--calendar-color) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__color {
|
&__color {
|
||||||
@@ -132,27 +139,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__options-btn {
|
&__options-btn {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--c--theme--colors--greyscale-500);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.15s, background-color 0.15s, color 0.15s;
|
transition:
|
||||||
|
opacity 0.15s,
|
||||||
&:hover {
|
background-color 0.15s,
|
||||||
background-color: var(--c--theme--colors--greyscale-200);
|
color 0.15s;
|
||||||
color: var(--c--theme--colors--greyscale-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-icons {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__menu {
|
&__menu {
|
||||||
@@ -272,7 +263,9 @@
|
|||||||
border: 3px solid transparent;
|
border: 3px solid transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.15s, box-shadow 0.15s;
|
transition:
|
||||||
|
transform 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: scale(1.15);
|
transform: scale(1.15);
|
||||||
@@ -280,7 +273,9 @@
|
|||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
border-color: #1f2937;
|
border-color: #1f2937;
|
||||||
box-shadow: 0 0 0 2px white, 0 0 0 4px #9ca3af;
|
box-shadow:
|
||||||
|
0 0 0 2px white,
|
||||||
|
0 0 0 4px #9ca3af;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,7 +7,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Checkbox } from "@gouvfr-lasuite/cunningham-react";
|
import { Checkbox } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
import { CalendarItemMenu } from "./CalendarItemMenu";
|
import { CalendarItemMenu } from "./CalendarItemMenu";
|
||||||
import type { CalendarListItemProps, SharedCalendarListItemProps } from "./types";
|
import type {
|
||||||
|
CalendarListItemProps,
|
||||||
|
SharedCalendarListItemProps,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CalendarListItem - Displays a user-owned calendar.
|
* CalendarListItem - Displays a user-owned calendar.
|
||||||
@@ -27,40 +30,35 @@ export const CalendarListItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="calendar-list__item">
|
<div className="calendar-list__item">
|
||||||
<div className="calendar-list__item-checkbox">
|
<div
|
||||||
|
className="calendar-list__item-checkbox"
|
||||||
|
style={{ "--calendar-color": calendar.color } as React.CSSProperties}
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isVisible}
|
checked={isVisible}
|
||||||
onChange={() => onToggleVisibility(calendar.url)}
|
onChange={() => onToggleVisibility(calendar.url)}
|
||||||
label=""
|
label=""
|
||||||
aria-label={`${t('calendar.list.showCalendar')} ${calendar.displayName || ''}`}
|
aria-label={`${t("calendar.list.showCalendar")} ${calendar.displayName || ""}`}
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="calendar-list__color"
|
|
||||||
style={{ backgroundColor: calendar.color }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className="calendar-list__name"
|
className="calendar-list__name"
|
||||||
title={calendar.displayName || undefined}
|
title={calendar.displayName || undefined}
|
||||||
>
|
>
|
||||||
{calendar.displayName || 'Sans nom'}
|
{calendar.displayName || "Sans nom"}
|
||||||
</span>
|
</span>
|
||||||
<div className="calendar-list__item-actions">
|
<div className="calendar-list__item-actions">
|
||||||
<button
|
<CalendarItemMenu
|
||||||
className="calendar-list__options-btn"
|
isOpen={isMenuOpen}
|
||||||
onClick={(e) => onMenuToggle(calendar.url, e)}
|
onOpenChange={(open) =>
|
||||||
aria-label="Options"
|
open ? onMenuToggle(calendar.url) : onCloseMenu()
|
||||||
>
|
}
|
||||||
<span className="material-icons">more_horiz</span>
|
onEdit={() => onEdit(calendar)}
|
||||||
</button>
|
onDelete={() => onDelete(calendar)}
|
||||||
{isMenuOpen && (
|
onSubscription={
|
||||||
<CalendarItemMenu
|
onSubscription ? () => onSubscription(calendar) : undefined
|
||||||
onEdit={() => onEdit(calendar)}
|
}
|
||||||
onDelete={() => onDelete(calendar)}
|
/>
|
||||||
onSubscription={onSubscription ? () => onSubscription(calendar) : undefined}
|
|
||||||
onClose={onCloseMenu}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -78,12 +76,15 @@ export const SharedCalendarListItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="calendar-list__item">
|
<div className="calendar-list__item">
|
||||||
<div className="calendar-list__item-checkbox">
|
<div
|
||||||
|
className="calendar-list__item-checkbox"
|
||||||
|
style={{ "--calendar-color": calendar.color } as React.CSSProperties}
|
||||||
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isVisible}
|
checked={isVisible}
|
||||||
onChange={() => onToggleVisibility(String(calendar.id))}
|
onChange={() => onToggleVisibility(String(calendar.id))}
|
||||||
label=""
|
label=""
|
||||||
aria-label={`${t('calendar.list.showCalendar')} ${calendar.name}`}
|
aria-label={`${t("calendar.list.showCalendar")} ${calendar.name}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="calendar-list__color"
|
className="calendar-list__color"
|
||||||
|
|||||||
@@ -147,8 +147,7 @@ export const useCalendarListState = ({
|
|||||||
|
|
||||||
// Menu handlers
|
// Menu handlers
|
||||||
const handleMenuToggle = useCallback(
|
const handleMenuToggle = useCallback(
|
||||||
(calendarUrl: string, e: React.MouseEvent) => {
|
(calendarUrl: string) => {
|
||||||
e.stopPropagation();
|
|
||||||
setOpenMenuUrl(openMenuUrl === calendarUrl ? null : calendarUrl);
|
setOpenMenuUrl(openMenuUrl === calendarUrl ? null : calendarUrl);
|
||||||
},
|
},
|
||||||
[openMenuUrl]
|
[openMenuUrl]
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ export interface CalendarModalProps {
|
|||||||
* Props for the CalendarItemMenu component.
|
* Props for the CalendarItemMenu component.
|
||||||
*/
|
*/
|
||||||
export interface CalendarItemMenuProps {
|
export interface CalendarItemMenuProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
onEdit: () => void;
|
onEdit: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onSubscription?: () => void;
|
onSubscription?: () => void;
|
||||||
onClose: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +47,7 @@ export interface CalendarListItemProps {
|
|||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
onToggleVisibility: (url: string) => void;
|
onToggleVisibility: (url: string) => void;
|
||||||
onMenuToggle: (url: string, e: React.MouseEvent) => void;
|
onMenuToggle: (url: string) => void;
|
||||||
onEdit: (calendar: CalDavCalendar) => void;
|
onEdit: (calendar: CalDavCalendar) => void;
|
||||||
onDelete: (calendar: CalDavCalendar) => void;
|
onDelete: (calendar: CalDavCalendar) => void;
|
||||||
onSubscription?: (calendar: CalDavCalendar) => void;
|
onSubscription?: (calendar: CalDavCalendar) => void;
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { LeftPanel } from "./LeftPanel";
|
export { LeftPanel, MiniCalendar } from "./left-panel";
|
||||||
export { MiniCalendar } from "./MiniCalendar";
|
export { AttendeesInput } from "./scheduler";
|
||||||
export { AttendeesInput } from "./AttendeesInput";
|
|
||||||
|
|||||||
@@ -3,16 +3,22 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: var(--c--theme--colors--greyscale-000);
|
background-color: var(--c--theme--colors--greyscale-000);
|
||||||
border-right: 1px solid
|
|
||||||
var(--c--contextuals--border--semantic--neutral--tertiary);
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
&__create {
|
&__create {
|
||||||
padding: 1rem 0.75rem;
|
padding: 0.75rem 0.75rem;
|
||||||
|
|
||||||
.c__button {
|
.c__button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mini-calendar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
align-self: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* LeftPanel component - Calendar sidebar with mini calendar and calendar list.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useMemo } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Button, useModal } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
import { IcsEvent } from "ts-ics";
|
||||||
|
|
||||||
|
import { CalendarList } from "../calendar-list";
|
||||||
|
import { MiniCalendar } from "./MiniCalendar";
|
||||||
|
import { EventModal } from "../scheduler/EventModal";
|
||||||
|
import { useCalendarContext } from "../../contexts";
|
||||||
|
import { useCalendars } from "../../hooks/useCalendars";
|
||||||
|
|
||||||
|
const BROWSER_TIMEZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rounded start and end times for a new event.
|
||||||
|
* Rounds down to the current hour, end is 1 hour later.
|
||||||
|
* Example: 14:30 -> start: 14:00, end: 15:00
|
||||||
|
*/
|
||||||
|
const getDefaultEventTimes = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const start = new Date(now);
|
||||||
|
start.setMinutes(0, 0, 0);
|
||||||
|
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setHours(end.getHours() + 1);
|
||||||
|
|
||||||
|
return { start, end };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftPanel = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
const {
|
||||||
|
selectedDate,
|
||||||
|
setSelectedDate,
|
||||||
|
davCalendars,
|
||||||
|
caldavService,
|
||||||
|
adapter,
|
||||||
|
calendarRef,
|
||||||
|
} = useCalendarContext();
|
||||||
|
|
||||||
|
const { data: calendars = [] } = useCalendars();
|
||||||
|
|
||||||
|
// Get default calendar URL
|
||||||
|
const defaultCalendarUrl = davCalendars[0]?.url || "";
|
||||||
|
|
||||||
|
// Create default event with rounded times
|
||||||
|
const defaultEvent = useMemo(() => {
|
||||||
|
const { start, end } = getDefaultEventTimes();
|
||||||
|
|
||||||
|
// Create "fake UTC" dates for the adapter
|
||||||
|
const fakeUtcStart = new Date(
|
||||||
|
Date.UTC(
|
||||||
|
start.getFullYear(),
|
||||||
|
start.getMonth(),
|
||||||
|
start.getDate(),
|
||||||
|
start.getHours(),
|
||||||
|
start.getMinutes(),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const fakeUtcEnd = new Date(
|
||||||
|
Date.UTC(
|
||||||
|
end.getFullYear(),
|
||||||
|
end.getMonth(),
|
||||||
|
end.getDate(),
|
||||||
|
end.getHours(),
|
||||||
|
end.getMinutes(),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: {
|
||||||
|
date: fakeUtcStart,
|
||||||
|
type: "DATE-TIME" as const,
|
||||||
|
local: {
|
||||||
|
date: fakeUtcStart,
|
||||||
|
timezone: BROWSER_TIMEZONE,
|
||||||
|
tzoffset: adapter.getTimezoneOffset(start, BROWSER_TIMEZONE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
date: fakeUtcEnd,
|
||||||
|
type: "DATE-TIME" as const,
|
||||||
|
local: {
|
||||||
|
date: fakeUtcEnd,
|
||||||
|
timezone: BROWSER_TIMEZONE,
|
||||||
|
tzoffset: adapter.getTimezoneOffset(end, BROWSER_TIMEZONE),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [adapter]);
|
||||||
|
|
||||||
|
// Handle save event
|
||||||
|
const handleSave = useCallback(
|
||||||
|
async (event: IcsEvent, calendarUrl: string) => {
|
||||||
|
const result = await caldavService.createEvent({
|
||||||
|
calendarUrl,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(result.error || "Failed to create event");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the calendar view
|
||||||
|
if (calendarRef.current) {
|
||||||
|
calendarRef.current.refetchEvents();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[caldavService, calendarRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
modal.close();
|
||||||
|
}, [modal]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="calendar-left-panel">
|
||||||
|
<div className="calendar-left-panel__create">
|
||||||
|
<Button
|
||||||
|
onClick={modal.open}
|
||||||
|
icon={<span className="material-icons">add</span>}
|
||||||
|
>
|
||||||
|
{t("calendar.leftPanel.newEvent")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MiniCalendar
|
||||||
|
selectedDate={selectedDate}
|
||||||
|
onDateSelect={setSelectedDate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="calendar-left-panel__divider" />
|
||||||
|
|
||||||
|
<CalendarList calendars={calendars} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{modal.isOpen && (
|
||||||
|
<EventModal
|
||||||
|
isOpen={modal.isOpen}
|
||||||
|
mode="create"
|
||||||
|
event={defaultEvent}
|
||||||
|
calendarUrl={defaultCalendarUrl}
|
||||||
|
calendars={davCalendars}
|
||||||
|
adapter={adapter}
|
||||||
|
onSave={handleSave}
|
||||||
|
onClose={handleClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
|
|
||||||
&__month-title {
|
&__month-title {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
color: var(--c--theme--colors--greyscale-800);
|
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav {
|
&__nav {
|
||||||
@@ -23,29 +23,6 @@
|
|||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__nav-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 1.75rem;
|
|
||||||
height: 1.75rem;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--c--theme--colors--greyscale-500);
|
|
||||||
transition: background-color 0.15s, color 0.15s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--c--theme--colors--greyscale-100);
|
|
||||||
color: var(--c--theme--colors--greyscale-700);
|
|
||||||
}
|
|
||||||
|
|
||||||
.material-icons {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__grid {
|
&__grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -63,15 +40,15 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
font-size: 0.75rem;
|
font-size: var(--c--globals--font--sizes--s);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--c--theme--colors--greyscale-800);
|
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
|
|
||||||
&--week-num {
|
&--week-num {
|
||||||
color: var(--c--theme--colors--greyscale-500);
|
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.7rem;
|
font-size: 0.6rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +68,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 0.75rem;
|
font-size: 0.6rem;
|
||||||
color: var(--c--theme--colors--greyscale-400);
|
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,32 +84,53 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--c--theme--colors--greyscale-800);
|
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: background-color 0.15s;
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--c--theme--colors--greyscale-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--outside {
|
&--outside {
|
||||||
color: var(--c--theme--colors--greyscale-400);
|
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
|
&.mini-calendar__day--today {
|
||||||
|
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mini-calendar__day--selected {
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--today {
|
&--today:not(.mini-calendar__day--outside) {
|
||||||
color: var(--c--theme--colors--primary-600);
|
color: var(--c--contextuals--content--semantic--brand--primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// &:not(&--outside):--today {
|
||||||
|
// color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||||
|
// font-weight: 600;
|
||||||
|
// }
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(
|
||||||
|
--c--contextuals--background--semantic--neutral--tertiary
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
&--selected {
|
&--selected {
|
||||||
background-color: #8b4513;
|
background-color: var(
|
||||||
|
--c--contextuals--background--semantic--brand--primary
|
||||||
|
);
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #7a3d11;
|
background-color: var(
|
||||||
|
--c--contextuals--background--semantic--brand--primary-hover
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,9 @@ import {
|
|||||||
subMonths,
|
subMonths,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useCalendarContext } from "../contexts";
|
import { useCalendarContext } from "../../contexts";
|
||||||
import { useCalendarLocale } from "../hooks/useCalendarLocale";
|
import { useCalendarLocale } from "../../hooks/useCalendarLocale";
|
||||||
|
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
interface MiniCalendarProps {
|
interface MiniCalendarProps {
|
||||||
selectedDate: Date;
|
selectedDate: Date;
|
||||||
@@ -71,13 +72,13 @@ export const MiniCalendar = ({
|
|||||||
// Generate weekday labels based on locale and first day of week
|
// Generate weekday labels based on locale and first day of week
|
||||||
const weekDays = useMemo(() => {
|
const weekDays = useMemo(() => {
|
||||||
const days = [
|
const days = [
|
||||||
t('calendar.recurrence.weekdays.mo'),
|
t("calendar.recurrence.weekdays.mo"),
|
||||||
t('calendar.recurrence.weekdays.tu'),
|
t("calendar.recurrence.weekdays.tu"),
|
||||||
t('calendar.recurrence.weekdays.we'),
|
t("calendar.recurrence.weekdays.we"),
|
||||||
t('calendar.recurrence.weekdays.th'),
|
t("calendar.recurrence.weekdays.th"),
|
||||||
t('calendar.recurrence.weekdays.fr'),
|
t("calendar.recurrence.weekdays.fr"),
|
||||||
t('calendar.recurrence.weekdays.sa'),
|
t("calendar.recurrence.weekdays.sa"),
|
||||||
t('calendar.recurrence.weekdays.su'),
|
t("calendar.recurrence.weekdays.su"),
|
||||||
];
|
];
|
||||||
// Rotate array based on firstDayOfWeek (0 = Sunday, 1 = Monday)
|
// Rotate array based on firstDayOfWeek (0 = Sunday, 1 = Monday)
|
||||||
if (firstDayOfWeek === 0) {
|
if (firstDayOfWeek === 0) {
|
||||||
@@ -106,20 +107,23 @@ export const MiniCalendar = ({
|
|||||||
{format(viewDate, "MMMM yyyy", { locale: dateFnsLocale })}
|
{format(viewDate, "MMMM yyyy", { locale: dateFnsLocale })}
|
||||||
</span>
|
</span>
|
||||||
<div className="mini-calendar__nav">
|
<div className="mini-calendar__nav">
|
||||||
<button
|
<Button
|
||||||
className="mini-calendar__nav-btn"
|
variant="tertiary"
|
||||||
|
size="small"
|
||||||
|
color="neutral"
|
||||||
onClick={handlePrevMonth}
|
onClick={handlePrevMonth}
|
||||||
|
icon={<span className="material-icons">chevron_left</span>}
|
||||||
aria-label={t("calendar.miniCalendar.previousMonth")}
|
aria-label={t("calendar.miniCalendar.previousMonth")}
|
||||||
>
|
/>
|
||||||
<span className="material-icons">chevron_left</span>
|
|
||||||
</button>
|
<Button
|
||||||
<button
|
variant="tertiary"
|
||||||
className="mini-calendar__nav-btn"
|
size="small"
|
||||||
|
color="neutral"
|
||||||
onClick={handleNextMonth}
|
onClick={handleNextMonth}
|
||||||
|
icon={<span className="material-icons">chevron_right</span>}
|
||||||
aria-label={t("calendar.miniCalendar.nextMonth")}
|
aria-label={t("calendar.miniCalendar.nextMonth")}
|
||||||
>
|
/>
|
||||||
<span className="material-icons">chevron_right</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -155,7 +159,9 @@ export const MiniCalendar = ({
|
|||||||
className={`mini-calendar__day ${
|
className={`mini-calendar__day ${
|
||||||
!isCurrentMonth ? "mini-calendar__day--outside" : ""
|
!isCurrentMonth ? "mini-calendar__day--outside" : ""
|
||||||
} ${isSelected ? "mini-calendar__day--selected" : ""} ${
|
} ${isSelected ? "mini-calendar__day--selected" : ""} ${
|
||||||
isToday && !isSelected ? "mini-calendar__day--today" : ""
|
isToday && !isSelected
|
||||||
|
? "mini-calendar__day--today"
|
||||||
|
: ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleDayClick(day)}
|
onClick={() => handleDayClick(day)}
|
||||||
>
|
>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { LeftPanel } from "./LeftPanel";
|
||||||
|
export { MiniCalendar } from "./MiniCalendar";
|
||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
} from "@gouvfr-lasuite/cunningham-react";
|
} from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
|
||||||
import { useAuth } from "@/features/auth/Auth";
|
import { useAuth } from "@/features/auth/Auth";
|
||||||
import { AttendeesInput } from "../AttendeesInput";
|
import { AttendeesInput } from "./AttendeesInput";
|
||||||
import { RecurrenceEditor } from "../RecurrenceEditor";
|
import { RecurrenceEditor } from "./RecurrenceEditor";
|
||||||
import { DeleteEventModal } from "./DeleteEventModal";
|
import { DeleteEventModal } from "./DeleteEventModal";
|
||||||
import type { EventModalProps, RecurringDeleteOption } from "./types";
|
import type { EventModalProps, RecurringDeleteOption } from "./types";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -98,7 +98,11 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
|||||||
|
|
||||||
// Callback to update toolbar state when calendar dates/view changes
|
// Callback to update toolbar state when calendar dates/view changes
|
||||||
const handleDatesSet = useCallback(
|
const handleDatesSet = useCallback(
|
||||||
(info: { start: Date; end: Date; view?: { type: string; title: string } }) => {
|
(info: {
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
view?: { type: string; title: string };
|
||||||
|
}) => {
|
||||||
// Update current date for MiniCalendar sync
|
// Update current date for MiniCalendar sync
|
||||||
const midTime = (info.start.getTime() + info.end.getTime()) / 2;
|
const midTime = (info.start.getTime() + info.end.getTime()) / 2;
|
||||||
setCurrentDate(new Date(midTime));
|
setCurrentDate(new Date(midTime));
|
||||||
@@ -112,7 +116,7 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setCurrentDate, calendarRef]
|
[setCurrentDate, calendarRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize calendar
|
// Initialize calendar
|
||||||
@@ -171,7 +175,7 @@ export const Scheduler = ({ defaultCalendarUrl }: SchedulerProps) => {
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
id="event-calendar"
|
id="event-calendar"
|
||||||
className="scheduler__calendar"
|
className="scheduler__calendar"
|
||||||
style={{ height: "calc(100vh - 160px)" }}
|
style={{ height: "calc(100vh - 52px - 90px)" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EventModal
|
<EventModal
|
||||||
|
|||||||
@@ -121,54 +121,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__view-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
min-width: 140px;
|
|
||||||
background: white;
|
|
||||||
border: 1px solid var(--c--globals--colors--gray-200);
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
padding: 0.25rem 0;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__view-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--c--globals--colors--gray-700);
|
|
||||||
transition: background-color 0.15s;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&--focused {
|
|
||||||
background-color: var(--c--globals--colors--gray-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
outline: 2px solid var(--c--globals--colors--brand-500);
|
|
||||||
outline-offset: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--selected {
|
|
||||||
color: var(--c--globals--colors--brand-500);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__view-check {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: var(--c--globals--colors--brand-500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -3,18 +3,14 @@
|
|||||||
* Replaces the native toolbar with React components using Cunningham design system.
|
* Replaces the native toolbar with React components using Cunningham design system.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo, useState, useRef, useEffect, useCallback } from "react";
|
import { useMemo, useState, useCallback } from "react";
|
||||||
|
|
||||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||||
|
import { DropdownMenu, type DropdownMenuOption } from "@gouvfr-lasuite/ui-kit";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { SchedulerToolbarProps } from "./types";
|
import type { SchedulerToolbarProps } from "./types";
|
||||||
|
|
||||||
type ViewOption = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SchedulerToolbar = ({
|
export const SchedulerToolbar = ({
|
||||||
calendarRef,
|
calendarRef,
|
||||||
currentView,
|
currentView,
|
||||||
@@ -23,22 +19,40 @@ export const SchedulerToolbar = ({
|
|||||||
}: SchedulerToolbarProps) => {
|
}: SchedulerToolbarProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isViewDropdownOpen, setIsViewDropdownOpen] = useState(false);
|
const [isViewDropdownOpen, setIsViewDropdownOpen] = useState(false);
|
||||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const isOpenRef = useRef(isViewDropdownOpen);
|
|
||||||
|
|
||||||
// Keep ref in sync with state to avoid stale closures
|
const handleViewChange = useCallback(
|
||||||
isOpenRef.current = isViewDropdownOpen;
|
(value: string) => {
|
||||||
|
calendarRef.current?.setOption("view", value);
|
||||||
|
onViewChange?.(value);
|
||||||
|
setIsViewDropdownOpen(false);
|
||||||
|
},
|
||||||
|
[calendarRef, onViewChange],
|
||||||
|
);
|
||||||
|
|
||||||
const viewOptions: ViewOption[] = useMemo(
|
const viewOptions: DropdownMenuOption[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ value: "timeGridDay", label: t("calendar.views.day") },
|
{
|
||||||
{ value: "timeGridWeek", label: t("calendar.views.week") },
|
value: "timeGridDay",
|
||||||
{ value: "dayGridMonth", label: t("calendar.views.month") },
|
label: t("calendar.views.day"),
|
||||||
{ value: "listWeek", label: t("calendar.views.listWeek") },
|
callback: () => handleViewChange("timeGridDay"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "timeGridWeek",
|
||||||
|
label: t("calendar.views.week"),
|
||||||
|
callback: () => handleViewChange("timeGridWeek"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "dayGridMonth",
|
||||||
|
label: t("calendar.views.month"),
|
||||||
|
callback: () => handleViewChange("dayGridMonth"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "listWeek",
|
||||||
|
label: t("calendar.views.listWeek"),
|
||||||
|
callback: () => handleViewChange("listWeek"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[t],
|
[t, handleViewChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentViewLabel = useMemo(() => {
|
const currentViewLabel = useMemo(() => {
|
||||||
@@ -46,55 +60,6 @@ export const SchedulerToolbar = ({
|
|||||||
return option?.label || t("calendar.views.week");
|
return option?.label || t("calendar.views.week");
|
||||||
}, [currentView, viewOptions, t]);
|
}, [currentView, viewOptions, t]);
|
||||||
|
|
||||||
// Handle click outside to close dropdown (uses ref to avoid stale closure)
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
isOpenRef.current &&
|
|
||||||
dropdownRef.current &&
|
|
||||||
!dropdownRef.current.contains(event.target as Node)
|
|
||||||
) {
|
|
||||||
setIsViewDropdownOpen(false);
|
|
||||||
setFocusedIndex(-1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Handle keyboard events for accessibility
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
|
||||||
if (!isOpenRef.current) return;
|
|
||||||
|
|
||||||
switch (event.key) {
|
|
||||||
case "Escape":
|
|
||||||
setIsViewDropdownOpen(false);
|
|
||||||
setFocusedIndex(-1);
|
|
||||||
triggerRef.current?.focus();
|
|
||||||
break;
|
|
||||||
case "ArrowDown":
|
|
||||||
event.preventDefault();
|
|
||||||
setFocusedIndex((prev) =>
|
|
||||||
prev < viewOptions.length - 1 ? prev + 1 : 0,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "ArrowUp":
|
|
||||||
event.preventDefault();
|
|
||||||
setFocusedIndex((prev) =>
|
|
||||||
prev > 0 ? prev - 1 : viewOptions.length - 1,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case "Tab":
|
|
||||||
setIsViewDropdownOpen(false);
|
|
||||||
setFocusedIndex(-1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
||||||
}, [viewOptions.length]);
|
|
||||||
|
|
||||||
const handleToday = useCallback(() => {
|
const handleToday = useCallback(() => {
|
||||||
calendarRef.current?.setOption("date", new Date());
|
calendarRef.current?.setOption("date", new Date());
|
||||||
}, [calendarRef]);
|
}, [calendarRef]);
|
||||||
@@ -107,55 +72,6 @@ export const SchedulerToolbar = ({
|
|||||||
calendarRef.current?.next();
|
calendarRef.current?.next();
|
||||||
}, [calendarRef]);
|
}, [calendarRef]);
|
||||||
|
|
||||||
const handleViewChange = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
calendarRef.current?.setOption("view", value);
|
|
||||||
onViewChange?.(value);
|
|
||||||
setIsViewDropdownOpen(false);
|
|
||||||
},
|
|
||||||
[calendarRef, onViewChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleViewDropdown = useCallback(() => {
|
|
||||||
setIsViewDropdownOpen((prev) => {
|
|
||||||
if (!prev) {
|
|
||||||
// Opening: set focus to current view
|
|
||||||
const currentIndex = viewOptions.findIndex(
|
|
||||||
(opt) => opt.value === currentView,
|
|
||||||
);
|
|
||||||
setFocusedIndex(currentIndex >= 0 ? currentIndex : 0);
|
|
||||||
} else {
|
|
||||||
setFocusedIndex(-1);
|
|
||||||
}
|
|
||||||
return !prev;
|
|
||||||
});
|
|
||||||
}, [viewOptions, currentView]);
|
|
||||||
|
|
||||||
const handleKeyDownOnTrigger = useCallback(
|
|
||||||
(event: React.KeyboardEvent) => {
|
|
||||||
if (event.key === "ArrowDown" && !isViewDropdownOpen) {
|
|
||||||
event.preventDefault();
|
|
||||||
setIsViewDropdownOpen(true);
|
|
||||||
const currentIndex = viewOptions.findIndex(
|
|
||||||
(opt) => opt.value === currentView,
|
|
||||||
);
|
|
||||||
setFocusedIndex(currentIndex >= 0 ? currentIndex : 0);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isViewDropdownOpen, viewOptions, currentView],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleKeyDownOnOption = useCallback(
|
|
||||||
(event: React.KeyboardEvent, value: string) => {
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
|
||||||
event.preventDefault();
|
|
||||||
handleViewChange(value);
|
|
||||||
triggerRef.current?.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[handleViewChange],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scheduler-toolbar">
|
<div className="scheduler-toolbar">
|
||||||
<div className="scheduler-toolbar__left">
|
<div className="scheduler-toolbar__left">
|
||||||
@@ -186,51 +102,31 @@ export const SchedulerToolbar = ({
|
|||||||
<h2 className="scheduler-toolbar__title">{viewTitle}</h2>
|
<h2 className="scheduler-toolbar__title">{viewTitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scheduler-toolbar__right" ref={dropdownRef}>
|
<div className="scheduler-toolbar__right">
|
||||||
<button
|
<DropdownMenu
|
||||||
ref={triggerRef}
|
options={viewOptions}
|
||||||
type="button"
|
isOpen={isViewDropdownOpen}
|
||||||
className="scheduler-toolbar__view-trigger"
|
onOpenChange={setIsViewDropdownOpen}
|
||||||
onClick={toggleViewDropdown}
|
selectedValues={[currentView]}
|
||||||
onKeyDown={handleKeyDownOnTrigger}
|
|
||||||
aria-expanded={isViewDropdownOpen}
|
|
||||||
aria-haspopup="listbox"
|
|
||||||
>
|
>
|
||||||
<span>{currentViewLabel}</span>
|
<Button
|
||||||
<span
|
iconPosition="right"
|
||||||
className={`material-icons scheduler-toolbar__view-arrow ${isViewDropdownOpen ? "scheduler-toolbar__view-arrow--open" : ""}`}
|
color="neutral"
|
||||||
>
|
variant="tertiary"
|
||||||
expand_more
|
onClick={() => setIsViewDropdownOpen(!isViewDropdownOpen)}
|
||||||
</span>
|
aria-expanded={isViewDropdownOpen}
|
||||||
</button>
|
icon={
|
||||||
|
<span
|
||||||
{isViewDropdownOpen && (
|
className={`material-icons scheduler-toolbar__view-arrow ${isViewDropdownOpen ? "scheduler-toolbar__view-arrow--open" : ""}`}
|
||||||
<div className="scheduler-toolbar__view-dropdown" role="listbox">
|
|
||||||
{viewOptions.map((option, index) => (
|
|
||||||
<button
|
|
||||||
key={option.value}
|
|
||||||
className={`scheduler-toolbar__view-option ${currentView === option.value ? "scheduler-toolbar__view-option--selected" : ""} ${focusedIndex === index ? "scheduler-toolbar__view-option--focused" : ""}`}
|
|
||||||
onClick={() => handleViewChange(option.value)}
|
|
||||||
onKeyDown={(e) => handleKeyDownOnOption(e, option.value)}
|
|
||||||
role="option"
|
|
||||||
aria-selected={currentView === option.value}
|
|
||||||
tabIndex={focusedIndex === index ? 0 : -1}
|
|
||||||
ref={(el) => {
|
|
||||||
if (focusedIndex === index && el) {
|
|
||||||
el.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{option.label}
|
expand_more
|
||||||
{currentView === option.value && (
|
</span>
|
||||||
<span className="material-icons scheduler-toolbar__view-check">
|
}
|
||||||
check
|
aria-haspopup="listbox"
|
||||||
</span>
|
>
|
||||||
)}
|
<span>{currentViewLabel}</span>
|
||||||
</button>
|
</Button>
|
||||||
))}
|
</DropdownMenu>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
export { Scheduler } from "./Scheduler";
|
export { Scheduler } from "./Scheduler";
|
||||||
export { EventModal } from "./EventModal";
|
export { EventModal } from "./EventModal";
|
||||||
export { DeleteEventModal } from "./DeleteEventModal";
|
export { DeleteEventModal } from "./DeleteEventModal";
|
||||||
|
export { AttendeesInput } from "./AttendeesInput";
|
||||||
|
export { RecurrenceEditor } from "./RecurrenceEditor";
|
||||||
export { useSchedulerHandlers } from "./hooks/useSchedulerHandlers";
|
export { useSchedulerHandlers } from "./hooks/useSchedulerHandlers";
|
||||||
export { useSchedulerInit, useSchedulingCapabilitiesCheck } from "./hooks/useSchedulerInit";
|
export { useSchedulerInit, useSchedulingCapabilitiesCheck } from "./hooks/useSchedulerInit";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
"errorServer": "Server error. Please try again later."
|
"errorServer": "Server error. Please try again later."
|
||||||
},
|
},
|
||||||
"leftPanel": {
|
"leftPanel": {
|
||||||
"create": "Create"
|
"newEvent": "New event"
|
||||||
},
|
},
|
||||||
"miniCalendar": {
|
"miniCalendar": {
|
||||||
"previousMonth": "Previous month",
|
"previousMonth": "Previous month",
|
||||||
@@ -731,7 +731,7 @@
|
|||||||
"errorServer": "Erreur serveur. Veuillez réessayer plus tard."
|
"errorServer": "Erreur serveur. Veuillez réessayer plus tard."
|
||||||
},
|
},
|
||||||
"leftPanel": {
|
"leftPanel": {
|
||||||
"create": "Créer"
|
"newEvent": "Nouvel événement"
|
||||||
},
|
},
|
||||||
"miniCalendar": {
|
"miniCalendar": {
|
||||||
"previousMonth": "Mois précédent",
|
"previousMonth": "Mois précédent",
|
||||||
@@ -1047,7 +1047,7 @@
|
|||||||
"errorServer": "Serverfout. Probeer het later opnieuw."
|
"errorServer": "Serverfout. Probeer het later opnieuw."
|
||||||
},
|
},
|
||||||
"leftPanel": {
|
"leftPanel": {
|
||||||
"create": "Aanmaken"
|
"newEvent": "Nieuw evenement"
|
||||||
},
|
},
|
||||||
"miniCalendar": {
|
"miniCalendar": {
|
||||||
"previousMonth": "Vorige maand",
|
"previousMonth": "Vorige maand",
|
||||||
|
|||||||
@@ -2,43 +2,23 @@
|
|||||||
* Calendar page - Main calendar view with sidebar.
|
* Calendar page - Main calendar view with sidebar.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback } from "react";
|
|
||||||
|
|
||||||
import { MainLayout } from "@gouvfr-lasuite/ui-kit";
|
import { MainLayout } from "@gouvfr-lasuite/ui-kit";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import { login, useAuth } from "@/features/auth/Auth";
|
import { login, useAuth } from "@/features/auth/Auth";
|
||||||
import { LeftPanel } from "@/features/calendar/components";
|
import { LeftPanel } from "@/features/calendar/components";
|
||||||
import { useCalendars } from "@/features/calendar/hooks/useCalendars";
|
|
||||||
import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout";
|
import { GlobalLayout } from "@/features/layouts/components/global/GlobalLayout";
|
||||||
import { HeaderRight } from "@/features/layouts/components/header/Header";
|
import { HeaderRight } from "@/features/layouts/components/header/Header";
|
||||||
import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage";
|
import { SpinnerPage } from "@/features/ui/components/spinner/SpinnerPage";
|
||||||
import { Toaster } from "@/features/ui/components/toaster/Toaster";
|
import { Toaster } from "@/features/ui/components/toaster/Toaster";
|
||||||
import { Scheduler } from "@/features/calendar/components/scheduler/Scheduler";
|
import { Scheduler } from "@/features/calendar/components/scheduler/Scheduler";
|
||||||
import { CalendarContextProvider, useCalendarContext } from "@/features/calendar/contexts";
|
import { CalendarContextProvider } from "@/features/calendar/contexts";
|
||||||
|
|
||||||
export default function CalendarPage() {
|
export default function CalendarPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
// Use selectedDate from context (the specific day user has clicked/selected)
|
|
||||||
// Note: currentDate (for view sync) is used directly by MiniCalendar
|
|
||||||
const { selectedDate, setSelectedDate } = useCalendarContext();
|
|
||||||
|
|
||||||
// Fetch calendars for the sidebar
|
|
||||||
const { data: calendars = [] } = useCalendars();
|
|
||||||
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
const handleDateSelect = useCallback((date: Date) => {
|
|
||||||
setSelectedDate(date);
|
|
||||||
}, [setSelectedDate]);
|
|
||||||
|
|
||||||
const handleCreateEvent = useCallback(() => {
|
|
||||||
console.log("handleCreateEvent");
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Redirect to login if not authenticated
|
// Redirect to login if not authenticated
|
||||||
if (!user) {
|
if (!user) {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -56,23 +36,11 @@ export default function CalendarPage() {
|
|||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
|
<div className="calendar-page">
|
||||||
<div className="calendar-page">
|
<div className="calendar-page__main">
|
||||||
<div className="calendar-page__sidebar">
|
<Scheduler />
|
||||||
<LeftPanel
|
|
||||||
calendars={calendars}
|
|
||||||
selectedDate={selectedDate}
|
|
||||||
onDateSelect={handleDateSelect}
|
|
||||||
onCreateEvent={handleCreateEvent}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="calendar-page__main">
|
|
||||||
<Scheduler />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</>
|
</>
|
||||||
@@ -82,23 +50,22 @@ export default function CalendarPage() {
|
|||||||
CalendarPage.getLayout = function getLayout(page: React.ReactElement) {
|
CalendarPage.getLayout = function getLayout(page: React.ReactElement) {
|
||||||
return (
|
return (
|
||||||
<CalendarContextProvider>
|
<CalendarContextProvider>
|
||||||
<div className="calendars__calendar">
|
<div className="calendars__calendar">
|
||||||
<GlobalLayout>
|
<GlobalLayout>
|
||||||
<MainLayout
|
<MainLayout
|
||||||
enableResize
|
enableResize={false}
|
||||||
hideLeftPanelOnDesktop={true}
|
leftPanelContent={<LeftPanel />}
|
||||||
leftPanelContent={null}
|
icon={
|
||||||
icon={
|
<div className="calendars__header__left">
|
||||||
<div className="calendars__header__left">
|
<div className="calendars__header__logo" />
|
||||||
<div className="calendars__header__logo" />
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
rightHeaderContent={<HeaderRight />}
|
||||||
rightHeaderContent={<HeaderRight />}
|
>
|
||||||
>
|
{page}
|
||||||
{page}
|
</MainLayout>
|
||||||
</MainLayout>
|
</GlobalLayout>
|
||||||
</GlobalLayout>
|
</div>
|
||||||
</div>
|
|
||||||
</CalendarContextProvider>
|
</CalendarContextProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
@use "./../features/ui/components/generic-disclaimer/GenericDisclaimer.scss";
|
@use "./../features/ui/components/generic-disclaimer/GenericDisclaimer.scss";
|
||||||
@use "./../features/ui/components/spinner/SpinnerPage.scss";
|
@use "./../features/ui/components/spinner/SpinnerPage.scss";
|
||||||
@use "./../features/layouts/components/left-panel/LeftPanelMobile.scss";
|
@use "./../features/layouts/components/left-panel/LeftPanelMobile.scss";
|
||||||
@use "./../features/calendar/components/MiniCalendar.scss";
|
@use "./../features/calendar/components/left-panel/MiniCalendar.scss";
|
||||||
@use "./../features/calendar/components/CalendarList.scss";
|
@use "./../features/calendar/components/calendar-list/CalendarList.scss";
|
||||||
@use "./../features/calendar/components/LeftPanel.scss";
|
@use "./../features/calendar/components/left-panel/LeftPanel.scss";
|
||||||
@use "./../features/calendar/components/EventModal.scss";
|
@use "./../features/calendar/components/scheduler/EventModal.scss";
|
||||||
@use "./../features/calendar/components/RecurrenceEditor.scss";
|
@use "./../features/calendar/components/scheduler/RecurrenceEditor.scss";
|
||||||
@use "./../features/calendar/components/AttendeesInput.scss";
|
@use "./../features/calendar/components/scheduler/AttendeesInput.scss";
|
||||||
@use "./../features/calendar/components/scheduler/Scheduler.scss";
|
@use "./../features/calendar/components/scheduler/Scheduler.scss";
|
||||||
@use "./../features/calendar/components/scheduler/scheduler-theme.scss";
|
@use "./../features/calendar/components/scheduler/scheduler-theme.scss";
|
||||||
@use "./../features/calendar/components/scheduler/SchedulerToolbar.scss";
|
@use "./../features/calendar/components/scheduler/SchedulerToolbar.scss";
|
||||||
|
|||||||
Reference in New Issue
Block a user