285 lines
9.0 KiB
TypeScript
285 lines
9.0 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
import { useParams, useRouter, useSearchParams } from "next/navigation"
|
|
import { addDays, addHours, addMonths, startOfHour } from "date-fns"
|
|
import { AgendaEventDialog, type AgendaEventDialogState } from "@/components/agenda/agenda-event-dialog"
|
|
import { AgendaEventPopover, type AgendaEventPopoverState } from "@/components/agenda/agenda-event-popover"
|
|
import type { AnchorRect } from "@/components/agenda/agenda-floating-card"
|
|
import { AgendaHeader } from "@/components/agenda/agenda-header"
|
|
import { AgendaQuickCreate, type AgendaQuickCreateState } from "@/components/agenda/agenda-quick-create"
|
|
import { AgendaSidebar } from "@/components/agenda/agenda-sidebar"
|
|
import { AgendaViewMonth } from "@/components/agenda/agenda-view-month"
|
|
import { AgendaViewWeek } from "@/components/agenda/agenda-view-week"
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
import { useAgendaCalendars, useAgendaEvents } from "@/lib/api/hooks/use-calendar-queries"
|
|
import { parseICSDate, viewDays, viewRange } from "@/lib/agenda/agenda-date"
|
|
import { useAgendaSettingsStore } from "@/lib/agenda/agenda-store"
|
|
import type { AgendaEvent, AgendaEventDraft } from "@/lib/agenda/agenda-types"
|
|
import {
|
|
buildAgendaPath,
|
|
parseAgendaSegments,
|
|
type AgendaView,
|
|
} from "@/lib/agenda/agenda-url"
|
|
import { useChromeIdentity } from "@/lib/hooks/use-chrome-identity"
|
|
import { useIsMobile } from "@/hooks/use-mobile"
|
|
|
|
export function AgendaPage() {
|
|
const router = useRouter()
|
|
const params = useParams()
|
|
const searchParams = useSearchParams()
|
|
const isMobile = useIsMobile()
|
|
const identity = useChromeIdentity()
|
|
|
|
const route = useMemo(
|
|
() => parseAgendaSegments(params.segments as string[] | undefined),
|
|
[params.segments],
|
|
)
|
|
const lastView = useAgendaSettingsStore((s) => s.lastView)
|
|
const setLastView = useAgendaSettingsStore((s) => s.setLastView)
|
|
const hiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds)
|
|
|
|
const view: AgendaView = route.view ?? (isMobile ? "day" : lastView)
|
|
const date = route.date
|
|
|
|
// Normalise l'URL quand la vue est implicite.
|
|
useEffect(() => {
|
|
if (!route.view) {
|
|
router.replace(buildAgendaPath(view, date) + window.location.search)
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [route.view])
|
|
|
|
useEffect(() => {
|
|
if (route.view) setLastView(route.view)
|
|
}, [route.view, setLastView])
|
|
|
|
const navigate = useCallback(
|
|
(nextView: AgendaView, nextDate: Date) => {
|
|
router.push(buildAgendaPath(nextView, nextDate))
|
|
},
|
|
[router],
|
|
)
|
|
|
|
const step = useCallback(
|
|
(delta: 1 | -1) => {
|
|
const next =
|
|
view === "month"
|
|
? addMonths(date, delta)
|
|
: addDays(date, view === "week" ? delta * 7 : delta)
|
|
navigate(view, next)
|
|
},
|
|
[view, date, navigate],
|
|
)
|
|
|
|
// Données
|
|
const { data: calendars = [], isLoading: calendarsLoading } = useAgendaCalendars()
|
|
const visibleCalendars = useMemo(
|
|
() => calendars.filter((c) => !hiddenIds.includes(c.id)),
|
|
[calendars, hiddenIds],
|
|
)
|
|
const fetchRange = useMemo(() => viewRange("month", date), [date])
|
|
const { events } = useAgendaEvents(visibleCalendars, fetchRange.start, fetchRange.end)
|
|
|
|
// UI : création rapide / dialog / détails
|
|
const [quickCreate, setQuickCreate] = useState<AgendaQuickCreateState | null>(null)
|
|
const [dialogState, setDialogState] = useState<AgendaEventDialogState | null>(null)
|
|
const [popover, setPopover] = useState<AgendaEventPopoverState | null>(null)
|
|
|
|
const defaultCalendarId = visibleCalendars[0]?.id ?? calendars[0]?.id ?? ""
|
|
const userEmail = identity?.email
|
|
|
|
const closeOverlays = useCallback(() => {
|
|
setQuickCreate(null)
|
|
setPopover(null)
|
|
}, [])
|
|
|
|
const openCreateDialog = useCallback(
|
|
(base?: Partial<AgendaEventDraft>) => {
|
|
closeOverlays()
|
|
const start = base?.start ?? addHours(startOfHour(new Date()), 1)
|
|
const end = base?.end ?? addHours(start, 1)
|
|
setDialogState({
|
|
mode: "create",
|
|
draft: {
|
|
title: "",
|
|
start,
|
|
end,
|
|
allDay: false,
|
|
calendarId: defaultCalendarId,
|
|
...base,
|
|
},
|
|
})
|
|
},
|
|
[closeOverlays, defaultCalendarId],
|
|
)
|
|
|
|
const openEditDialog = useCallback(
|
|
(event: AgendaEvent) => {
|
|
closeOverlays()
|
|
const masterStart = parseICSDate(event.master.start) ?? event.start
|
|
const masterEnd = parseICSDate(event.master.end) ?? event.end
|
|
setDialogState({
|
|
mode: "edit",
|
|
event,
|
|
draft: {
|
|
title: event.title === "(Sans titre)" ? "" : event.title,
|
|
start: masterStart,
|
|
end: masterEnd,
|
|
allDay: event.allDay,
|
|
calendarId: event.calendarId,
|
|
description: event.description,
|
|
location: event.location,
|
|
attendees: event.attendees,
|
|
rrule: event.rrule,
|
|
color: event.master.color,
|
|
},
|
|
})
|
|
},
|
|
[closeOverlays],
|
|
)
|
|
|
|
// Interop : /agenda?new=1&guest=…&title=…
|
|
const handledNewParam = useRef(false)
|
|
useEffect(() => {
|
|
if (handledNewParam.current || !route.view) return
|
|
if (searchParams.get("new") !== "1") return
|
|
handledNewParam.current = true
|
|
const guest = searchParams.get("guest")?.trim()
|
|
const guestName = searchParams.get("guest_name")?.trim()
|
|
openCreateDialog({
|
|
title: searchParams.get("title")?.trim() ?? "",
|
|
attendees: guest
|
|
? [{ email: guest, name: guestName || guest, status: "NEEDS-ACTION" }]
|
|
: [],
|
|
})
|
|
router.replace(buildAgendaPath(view, date))
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [searchParams, route.view])
|
|
|
|
// Raccourcis clavier façon Google Calendar.
|
|
useEffect(() => {
|
|
const onKey = (e: KeyboardEvent) => {
|
|
const target = e.target as HTMLElement
|
|
if (
|
|
e.metaKey ||
|
|
e.ctrlKey ||
|
|
e.altKey ||
|
|
target.closest("input, textarea, select, [contenteditable], [role=dialog]")
|
|
)
|
|
return
|
|
switch (e.key.toLowerCase()) {
|
|
case "j":
|
|
case "d":
|
|
navigate("day", date)
|
|
break
|
|
case "s":
|
|
case "w":
|
|
navigate("week", date)
|
|
break
|
|
case "m":
|
|
navigate("month", date)
|
|
break
|
|
case "t":
|
|
navigate(view, new Date())
|
|
break
|
|
case "c":
|
|
openCreateDialog()
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
e.preventDefault()
|
|
}
|
|
window.addEventListener("keydown", onKey)
|
|
return () => window.removeEventListener("keydown", onKey)
|
|
}, [navigate, view, date, openCreateDialog])
|
|
|
|
const handleEventClick = useCallback((event: AgendaEvent, anchor: AnchorRect) => {
|
|
setQuickCreate(null)
|
|
setPopover({ event, anchor })
|
|
}, [])
|
|
|
|
const handleCreateRange = useCallback(
|
|
(start: Date, end: Date, allDay: boolean, anchor: AnchorRect) => {
|
|
setPopover(null)
|
|
setQuickCreate({ start, end, allDay, anchor })
|
|
},
|
|
[],
|
|
)
|
|
|
|
const days = useMemo(() => viewDays(view, date), [view, date])
|
|
|
|
return (
|
|
<>
|
|
<AgendaHeader
|
|
view={view}
|
|
date={date}
|
|
onToday={() => navigate(view, new Date())}
|
|
onStep={step}
|
|
onViewChange={(v) => navigate(v, date)}
|
|
/>
|
|
<div className="flex min-h-0 flex-1">
|
|
<AgendaSidebar
|
|
selectedDate={date}
|
|
onSelectDate={(d) => navigate(view, d)}
|
|
onCreateEvent={() => openCreateDialog()}
|
|
/>
|
|
<main className="flex min-w-0 flex-1 flex-col">
|
|
{calendarsLoading ? (
|
|
<div className="flex h-full flex-col gap-3 p-6">
|
|
<Skeleton className="h-8 w-64" />
|
|
<Skeleton className="min-h-0 flex-1" />
|
|
</div>
|
|
) : view === "month" ? (
|
|
<AgendaViewMonth
|
|
date={date}
|
|
events={events}
|
|
onCreateAt={(day, anchor) =>
|
|
handleCreateRange(day, addDays(day, 1), true, anchor)
|
|
}
|
|
onEventClick={handleEventClick}
|
|
onOpenDay={(day) => navigate("day", day)}
|
|
/>
|
|
) : (
|
|
<AgendaViewWeek
|
|
days={days}
|
|
events={events}
|
|
onCreateRange={handleCreateRange}
|
|
onEventClick={handleEventClick}
|
|
onOpenDay={(day) => navigate("day", day)}
|
|
/>
|
|
)}
|
|
</main>
|
|
</div>
|
|
|
|
<AgendaQuickCreate
|
|
state={quickCreate}
|
|
calendars={visibleCalendars.length > 0 ? visibleCalendars : calendars}
|
|
defaultCalendarId={defaultCalendarId}
|
|
userEmail={userEmail}
|
|
onClose={() => setQuickCreate(null)}
|
|
onMoreOptions={(draft) => {
|
|
setQuickCreate(null)
|
|
setDialogState({ mode: "create", draft })
|
|
}}
|
|
/>
|
|
|
|
<AgendaEventPopover
|
|
state={popover}
|
|
calendars={calendars}
|
|
userEmail={userEmail}
|
|
onClose={() => setPopover(null)}
|
|
onEdit={openEditDialog}
|
|
/>
|
|
|
|
<AgendaEventDialog
|
|
state={dialogState}
|
|
onClose={() => setDialogState(null)}
|
|
calendars={calendars}
|
|
userEmail={userEmail}
|
|
/>
|
|
</>
|
|
)
|
|
}
|