✨(front) add mobile toolbar, week bar, FAB and list view
Add dedicated mobile components: MobileToolbar for navigation, WeekDayBar for day selection, FloatingActionButton for quick event creation, and MobileListView for agenda. Also add mobile navigation hook and translations.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { CalendarApi } from "../types";
|
||||
import { addDays, getWeekStart } from "@/utils/date";
|
||||
|
||||
interface UseMobileNavigationProps {
|
||||
currentDate: Date;
|
||||
firstDayOfWeek: number;
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
}
|
||||
|
||||
export function useMobileNavigation({
|
||||
currentDate,
|
||||
firstDayOfWeek,
|
||||
calendarRef,
|
||||
}: UseMobileNavigationProps) {
|
||||
const weekStart = useMemo(
|
||||
() => getWeekStart(currentDate, firstDayOfWeek),
|
||||
[currentDate, firstDayOfWeek],
|
||||
);
|
||||
|
||||
const weekDays = useMemo(() => {
|
||||
return Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
|
||||
}, [weekStart]);
|
||||
|
||||
const navigatePreservingScroll = useCallback(
|
||||
(date: Date) => {
|
||||
const ecBody = calendarRef.current
|
||||
? (document.querySelector(".ec-main") as HTMLElement | null)
|
||||
: null;
|
||||
const scrollTop = ecBody?.scrollTop ?? 0;
|
||||
|
||||
calendarRef.current?.setOption("date", date);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (ecBody) ecBody.scrollTop = scrollTop;
|
||||
});
|
||||
},
|
||||
[calendarRef],
|
||||
);
|
||||
|
||||
const handleWeekPrev = useCallback(() => {
|
||||
navigatePreservingScroll(addDays(currentDate, -7));
|
||||
}, [currentDate, navigatePreservingScroll]);
|
||||
|
||||
const handleWeekNext = useCallback(() => {
|
||||
navigatePreservingScroll(addDays(currentDate, 7));
|
||||
}, [currentDate, navigatePreservingScroll]);
|
||||
|
||||
const handleDayClick = useCallback(
|
||||
(date: Date) => {
|
||||
navigatePreservingScroll(date);
|
||||
},
|
||||
[navigatePreservingScroll],
|
||||
);
|
||||
|
||||
const handleTodayClick = useCallback(() => {
|
||||
calendarRef.current?.setOption("date", new Date());
|
||||
}, [calendarRef]);
|
||||
|
||||
return {
|
||||
weekStart,
|
||||
weekDays,
|
||||
handleWeekPrev,
|
||||
handleWeekNext,
|
||||
handleDayClick,
|
||||
handleTodayClick,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
.fab-create-event {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
border: none;
|
||||
background: var(--c--contextuals--background--semantic--brand--primary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 24px;
|
||||
color: var(--c--contextuals--content--semantic--neutral--on-neutral);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { FloatingActionButtonProps } from "../types";
|
||||
|
||||
export const FloatingActionButton = ({ onClick }: FloatingActionButtonProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<button
|
||||
className="fab-create-event"
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
aria-label={t("calendar.leftPanel.newEvent")}
|
||||
>
|
||||
<span className="material-icons fab-create-event__icon" aria-hidden="true">add</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
.mobile-list {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
&__day {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__day-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__day-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
flex-shrink: 0;
|
||||
|
||||
&--today {
|
||||
background: var(--c--contextuals--background--semantic--brand--primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__day-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
&__today-tag {
|
||||
background: var(--c--contextuals--background--semantic--brand--secondary);
|
||||
color: var(--c--contextuals--content--semantic--brand--primary);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&__events {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__event-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
border-radius: 10px;
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:active {
|
||||
background: var(--c--contextuals--background--surface--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&__color-strip {
|
||||
width: 4px;
|
||||
height: 40px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__event-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__event-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__event-time {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
font-size: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
background: var(--c--contextuals--background--surface--secondary);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&__empty-icon {
|
||||
font-size: 20px;
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
}
|
||||
|
||||
&__empty-text {
|
||||
font-size: 13px;
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { MobileListViewProps, MobileListEvent } from "../types";
|
||||
import { isSameDay, isToday } from "@/utils/date";
|
||||
|
||||
function groupEventsByDay(
|
||||
events: MobileListEvent[],
|
||||
weekDays: Date[],
|
||||
): Map<string, MobileListEvent[]> {
|
||||
const grouped = new Map<string, MobileListEvent[]>();
|
||||
for (const day of weekDays) {
|
||||
grouped.set(day.toISOString(), []);
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
for (const day of weekDays) {
|
||||
if (isSameDay(event.start, day)) {
|
||||
grouped.get(day.toISOString())?.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [, dayEvents] of grouped) {
|
||||
dayEvents.sort((a, b) => a.start.getTime() - b.start.getTime());
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
export const MobileListView = ({
|
||||
weekDays,
|
||||
events,
|
||||
intlLocale,
|
||||
onEventClick,
|
||||
}: MobileListViewProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const eventsByDay = useMemo(
|
||||
() => groupEventsByDay(events, weekDays),
|
||||
[events, weekDays],
|
||||
);
|
||||
|
||||
const dayHeaderFormatter = useMemo(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(intlLocale, {
|
||||
weekday: "long",
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
}),
|
||||
[intlLocale],
|
||||
);
|
||||
|
||||
const timeFormatter = useMemo(
|
||||
() =>
|
||||
new Intl.DateTimeFormat(intlLocale, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
[intlLocale],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mobile-list">
|
||||
{weekDays.map((day) => {
|
||||
const dayKey = day.toISOString();
|
||||
const dayEvents = eventsByDay.get(dayKey) ?? [];
|
||||
const dayIsToday = isToday(day);
|
||||
|
||||
return (
|
||||
<div key={dayKey} className="mobile-list__day">
|
||||
<div className="mobile-list__day-header">
|
||||
<span
|
||||
className={`mobile-list__day-dot ${
|
||||
dayIsToday ? "mobile-list__day-dot--today" : ""
|
||||
}`}
|
||||
/>
|
||||
<span className="mobile-list__day-title">
|
||||
{dayHeaderFormatter.format(day)}
|
||||
</span>
|
||||
{dayIsToday && (
|
||||
<span className="mobile-list__today-tag">
|
||||
{t("calendar.views.today")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dayEvents.length === 0 ? (
|
||||
<div className="mobile-list__empty">
|
||||
<span className="material-icons mobile-list__empty-icon">
|
||||
event_busy
|
||||
</span>
|
||||
<span className="mobile-list__empty-text">
|
||||
{t("calendar.views.mobile.noEvents")}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mobile-list__events">
|
||||
{dayEvents.map((event) => (
|
||||
<button
|
||||
key={String(event.id)}
|
||||
className="mobile-list__event-card"
|
||||
onClick={() =>
|
||||
onEventClick(String(event.id), event.extendedProps)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className="mobile-list__color-strip"
|
||||
style={{ backgroundColor: event.backgroundColor }}
|
||||
/>
|
||||
<div className="mobile-list__event-info">
|
||||
<span className="mobile-list__event-title">
|
||||
{event.title || t("calendar.event.titlePlaceholder")}
|
||||
</span>
|
||||
<span className="mobile-list__event-time">
|
||||
{event.allDay
|
||||
? t("calendar.event.allDay")
|
||||
: `${timeFormatter.format(event.start)} - ${timeFormatter.format(event.end)}`}
|
||||
</span>
|
||||
</div>
|
||||
<span className="material-icons mobile-list__chevron">
|
||||
chevron_right
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
.mobile-toolbar {
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
padding: 0 12px;
|
||||
gap: 4px;
|
||||
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
}
|
||||
|
||||
&__today-btn {
|
||||
border-radius: 20px !important;
|
||||
padding: 6px 12px !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
&__nav-arrows {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__date-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
}
|
||||
|
||||
&__view-wrapper {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__view-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 8px;
|
||||
border: 1.5px solid var(--c--contextuals--border--surface--primary);
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&:active {
|
||||
background: var(--c--contextuals--background--surface--secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&__view-arrow {
|
||||
font-size: 18px;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&--open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { useMemo, useState, useCallback } from "react";
|
||||
|
||||
import { Button } from "@gouvfr-lasuite/cunningham-react";
|
||||
import { DropdownMenu, type DropdownMenuOption } from "@gouvfr-lasuite/ui-kit";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useCalendarLocale } from "../../../hooks/useCalendarLocale";
|
||||
import type { MobileToolbarProps } from "../types";
|
||||
|
||||
function formatMobileTitle(
|
||||
currentDate: Date,
|
||||
intlLocale: string,
|
||||
): string {
|
||||
return new Intl.DateTimeFormat(intlLocale, {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}).format(currentDate);
|
||||
}
|
||||
|
||||
export const MobileToolbar = ({
|
||||
currentView,
|
||||
currentDate,
|
||||
onViewChange,
|
||||
onWeekPrev,
|
||||
onWeekNext,
|
||||
onTodayClick,
|
||||
}: MobileToolbarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { intlLocale } = useCalendarLocale();
|
||||
const [isViewDropdownOpen, setIsViewDropdownOpen] = useState(false);
|
||||
|
||||
const handleViewChange = useCallback(
|
||||
(value: string) => {
|
||||
onViewChange(value);
|
||||
setIsViewDropdownOpen(false);
|
||||
},
|
||||
[onViewChange],
|
||||
);
|
||||
|
||||
const viewOptions: DropdownMenuOption[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: "timeGridDay",
|
||||
label: t("calendar.views.mobile.oneDay"),
|
||||
callback: () => handleViewChange("timeGridDay"),
|
||||
},
|
||||
{
|
||||
value: "timeGridTwoDays",
|
||||
label: t("calendar.views.mobile.twoDays"),
|
||||
callback: () => handleViewChange("timeGridTwoDays"),
|
||||
},
|
||||
{
|
||||
value: "listWeek",
|
||||
label: t("calendar.views.mobile.list"),
|
||||
callback: () => handleViewChange("listWeek"),
|
||||
},
|
||||
],
|
||||
[t, handleViewChange],
|
||||
);
|
||||
|
||||
const currentViewLabel = useMemo(() => {
|
||||
const option = viewOptions.find((opt) => opt.value === currentView);
|
||||
return option?.label || t("calendar.views.mobile.oneDay");
|
||||
}, [currentView, viewOptions, t]);
|
||||
|
||||
const title = useMemo(
|
||||
() => formatMobileTitle(currentDate, intlLocale),
|
||||
[currentDate, intlLocale],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mobile-toolbar">
|
||||
<div className="mobile-toolbar__nav">
|
||||
<Button
|
||||
color="neutral"
|
||||
variant="bordered"
|
||||
size="small"
|
||||
onClick={onTodayClick}
|
||||
className="mobile-toolbar__today-btn"
|
||||
>
|
||||
{t("calendar.views.today")}
|
||||
</Button>
|
||||
|
||||
<div className="mobile-toolbar__nav-arrows">
|
||||
<Button
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={onWeekPrev}
|
||||
icon={<span className="material-icons">chevron_left</span>}
|
||||
aria-label={t("calendar.navigation.previous")}
|
||||
/>
|
||||
<Button
|
||||
color="neutral"
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={onWeekNext}
|
||||
icon={<span className="material-icons">chevron_right</span>}
|
||||
aria-label={t("calendar.navigation.next")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span className="mobile-toolbar__date-title">{title}</span>
|
||||
|
||||
<div className="mobile-toolbar__view-wrapper">
|
||||
<DropdownMenu
|
||||
options={viewOptions}
|
||||
isOpen={isViewDropdownOpen}
|
||||
onOpenChange={setIsViewDropdownOpen}
|
||||
selectedValues={[currentView]}
|
||||
>
|
||||
<button
|
||||
className="mobile-toolbar__view-selector"
|
||||
onClick={() => setIsViewDropdownOpen(!isViewDropdownOpen)}
|
||||
type="button"
|
||||
aria-expanded={isViewDropdownOpen}
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
<span>{currentViewLabel}</span>
|
||||
<span
|
||||
className={`material-icons mobile-toolbar__view-arrow ${
|
||||
isViewDropdownOpen ? "mobile-toolbar__view-arrow--open" : ""
|
||||
}`}
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
.week-day-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--c--contextuals--border--surface--primary);
|
||||
background: var(--c--contextuals--background--surface--primary);
|
||||
|
||||
// Row 1: day name letters
|
||||
&__names {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
}
|
||||
|
||||
&__day-name {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--c--contextuals--content--semantic--neutral--secondary);
|
||||
line-height: 1;
|
||||
|
||||
&--weekend {
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// Row 2: day numbers
|
||||
&__numbers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--c--contextuals--content--semantic--neutral--primary);
|
||||
height: 36px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&--weekend {
|
||||
color: var(--c--contextuals--content--semantic--neutral--tertiary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
// Selected: circle drawn as pseudo-element so flex: 1 stays
|
||||
&--selected {
|
||||
color: var(--c--contextuals--content--semantic--neutral--on-neutral);
|
||||
z-index: 1;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
background: var(--c--contextuals--background--semantic--brand--primary);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { useMemo } from "react";
|
||||
import type { WeekDayBarProps } from "../types";
|
||||
import { isSameDay, isWeekend, addDays } from "@/utils/date";
|
||||
|
||||
export const WeekDayBar = ({
|
||||
currentDate,
|
||||
currentView,
|
||||
intlLocale,
|
||||
weekDays,
|
||||
onDayClick,
|
||||
}: WeekDayBarProps) => {
|
||||
const today = useMemo(() => new Date(), []);
|
||||
|
||||
const narrowDayFormatter = useMemo(
|
||||
() => new Intl.DateTimeFormat(intlLocale, { weekday: "narrow" }),
|
||||
[intlLocale],
|
||||
);
|
||||
|
||||
const isTwoDays = currentView === "timeGridTwoDays";
|
||||
|
||||
const isSelected = (date: Date): boolean => {
|
||||
if (isTwoDays) {
|
||||
const nextDay = addDays(currentDate, 1);
|
||||
return isSameDay(date, currentDate) || isSameDay(date, nextDay);
|
||||
}
|
||||
if (currentView === "timeGridDay") {
|
||||
return isSameDay(date, currentDate);
|
||||
}
|
||||
if (currentView === "listWeek") {
|
||||
return isSameDay(date, today);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleClick = (date: Date) => {
|
||||
if (currentView !== "listWeek") {
|
||||
onDayClick(date);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="week-day-bar">
|
||||
<div className="week-day-bar__names">
|
||||
{weekDays.map((date) => (
|
||||
<span
|
||||
key={date.toISOString()}
|
||||
className={`week-day-bar__day-name${isWeekend(date) ? " week-day-bar__day-name--weekend" : ""}`}
|
||||
>
|
||||
{narrowDayFormatter.format(date)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="week-day-bar__numbers">
|
||||
{weekDays.map((date) => {
|
||||
const classes = [
|
||||
"week-day-bar__number",
|
||||
isSelected(date) && "week-day-bar__number--selected",
|
||||
isWeekend(date) && "week-day-bar__number--weekend",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
return (
|
||||
<button
|
||||
key={date.toISOString()}
|
||||
className={classes}
|
||||
onClick={() => handleClick(date)}
|
||||
type="button"
|
||||
>
|
||||
{date.getDate()}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -131,6 +131,15 @@ export interface CalendarApi {
|
||||
addEvent: (event: unknown) => void;
|
||||
unselect: () => void;
|
||||
refetchEvents: () => void;
|
||||
getEvents: () => Array<{
|
||||
id: string | number;
|
||||
title?: string | { html: string } | { domNodes: Node[] };
|
||||
start: Date | string;
|
||||
end?: Date | string;
|
||||
allDay?: boolean;
|
||||
backgroundColor?: string;
|
||||
extendedProps?: Record<string, unknown>;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,3 +151,59 @@ export interface SchedulerToolbarProps {
|
||||
viewTitle: string;
|
||||
onViewChange?: (view: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile-specific view types.
|
||||
*/
|
||||
export type MobileView = "timeGridDay" | "timeGridTwoDays" | "listWeek";
|
||||
|
||||
/**
|
||||
* Props for the MobileToolbar component.
|
||||
*/
|
||||
export interface MobileToolbarProps {
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
currentView: string;
|
||||
currentDate: Date;
|
||||
onViewChange: (view: string) => void;
|
||||
onWeekPrev: () => void;
|
||||
onWeekNext: () => void;
|
||||
onTodayClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the WeekDayBar component.
|
||||
*/
|
||||
export interface WeekDayBarProps {
|
||||
currentDate: Date;
|
||||
currentView: string;
|
||||
intlLocale: string;
|
||||
weekDays: Date[];
|
||||
onDayClick: (date: Date) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the FloatingActionButton component.
|
||||
*/
|
||||
export interface FloatingActionButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the MobileListView component.
|
||||
*/
|
||||
export interface MobileListEvent {
|
||||
id: string | number;
|
||||
title: string;
|
||||
start: Date;
|
||||
end: Date;
|
||||
allDay: boolean;
|
||||
backgroundColor: string;
|
||||
extendedProps: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MobileListViewProps {
|
||||
weekDays: Date[];
|
||||
events: MobileListEvent[];
|
||||
intlLocale: string;
|
||||
onEventClick: (eventId: string, extendedProps: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,13 @@
|
||||
"listWeek": "Week list",
|
||||
"listMonth": "Month list",
|
||||
"listYear": "Year list",
|
||||
"today": "Today"
|
||||
"today": "Today",
|
||||
"mobile": {
|
||||
"oneDay": "One day",
|
||||
"twoDays": "Two days",
|
||||
"list": "List",
|
||||
"noEvents": "No events"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"previous": "Previous",
|
||||
@@ -982,7 +988,13 @@
|
||||
"listWeek": "Liste semaine",
|
||||
"listMonth": "Liste mois",
|
||||
"listYear": "Liste année",
|
||||
"today": "Aujourd'hui"
|
||||
"today": "Aujourd'hui",
|
||||
"mobile": {
|
||||
"oneDay": "Un jour",
|
||||
"twoDays": "2 jours",
|
||||
"list": "Liste",
|
||||
"noEvents": "Aucun événement"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"previous": "Précédent",
|
||||
@@ -1575,7 +1587,13 @@
|
||||
"listWeek": "Week lijst",
|
||||
"listMonth": "Maand lijst",
|
||||
"listYear": "Jaar lijst",
|
||||
"today": "Vandaag"
|
||||
"today": "Vandaag",
|
||||
"mobile": {
|
||||
"oneDay": "Een dag",
|
||||
"twoDays": "Twee dagen",
|
||||
"list": "Lijst",
|
||||
"noEvents": "Geen evenementen"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"previous": "Vorige",
|
||||
|
||||
Reference in New Issue
Block a user