Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced turbopack alias for canvas in next.config.mjs. - Updated package.json scripts for development and branding tasks. - Added new dependencies for Tiptap extensions. - Implemented new demo layouts for agenda, contacts, drive, and mail applications. - Enhanced globals.css for improved theming and splash screen animations. - Added OAuth callback handling for drive mounts. - Updated layout components to integrate new demo shells and improve structure.
243 lines
7.6 KiB
TypeScript
243 lines
7.6 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
import { toast } from "sonner"
|
|
import { Users, Video, X } from "lucide-react"
|
|
import { AgendaEventScheduleFields } from "@/components/agenda/agenda-event-schedule-fields"
|
|
import { AgendaGuestPicker } from "@/components/agenda/agenda-guest-picker"
|
|
import { AgendaVideoToggle } from "@/components/agenda/agenda-video-toggle"
|
|
import { AgendaFloatingCard, type AnchorRect } from "@/components/agenda/agenda-floating-card"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Input } from "@/components/ui/input"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { useCreateAgendaEvent, useCreateAgendaMeetLink } from "@/lib/api/hooks/use-calendar-mutations"
|
|
import { createAgendaEventWithVideo } from "@/lib/agenda/agenda-save-with-video"
|
|
import { calendarColor } from "@/lib/agenda/agenda-events"
|
|
import { useEffectiveAgendaSettings } from "@/lib/agenda/use-effective-agenda-settings"
|
|
import type {
|
|
AgendaCalendar,
|
|
AgendaEventAttendee,
|
|
AgendaEventDraft,
|
|
} from "@/lib/agenda/agenda-types"
|
|
|
|
export interface AgendaQuickCreateState {
|
|
start: Date
|
|
end: Date
|
|
allDay: boolean
|
|
anchor: AnchorRect
|
|
}
|
|
|
|
export function AgendaQuickCreate({
|
|
state,
|
|
calendars,
|
|
defaultCalendarId,
|
|
userEmail,
|
|
onClose,
|
|
onMoreOptions,
|
|
}: {
|
|
state: AgendaQuickCreateState | null
|
|
calendars: AgendaCalendar[]
|
|
defaultCalendarId: string
|
|
userEmail?: string
|
|
onClose: () => void
|
|
onMoreOptions: (draft: AgendaEventDraft) => void
|
|
}) {
|
|
const [title, setTitle] = useState("")
|
|
const [calendarId, setCalendarId] = useState(defaultCalendarId)
|
|
const [start, setStart] = useState(() => new Date())
|
|
const [end, setEnd] = useState(() => new Date())
|
|
const [attendees, setAttendees] = useState<AgendaEventAttendee[]>([])
|
|
const [includeVideo, setIncludeVideo] = useState(false)
|
|
const titleRef = useRef<HTMLInputElement>(null)
|
|
const createMutation = useCreateAgendaEvent()
|
|
const meetLinkMutation = useCreateAgendaMeetLink()
|
|
const { buttonSnapMinutes, defaultVideoProvider } = useEffectiveAgendaSettings()
|
|
|
|
useEffect(() => {
|
|
if (!state) return
|
|
setTitle("")
|
|
setCalendarId(defaultCalendarId)
|
|
setStart(state.start)
|
|
setEnd(state.end)
|
|
setAttendees([])
|
|
setIncludeVideo(false)
|
|
const timer = window.setTimeout(() => titleRef.current?.focus(), 0)
|
|
return () => window.clearTimeout(timer)
|
|
}, [state, defaultCalendarId])
|
|
|
|
if (!state) return null
|
|
|
|
const calendar = calendars.find((c) => c.id === calendarId) ?? calendars[0]
|
|
const draft: AgendaEventDraft = {
|
|
title,
|
|
start,
|
|
end,
|
|
allDay: state.allDay,
|
|
calendarId,
|
|
attendees,
|
|
includeVideo,
|
|
}
|
|
|
|
const save = async () => {
|
|
if (!calendarId || !calendar) return
|
|
try {
|
|
await createAgendaEventWithVideo({
|
|
draft,
|
|
calendar,
|
|
userEmail,
|
|
includeVideo: includeVideo && !state.allDay,
|
|
videoProvider: defaultVideoProvider,
|
|
createMutation,
|
|
meetLinkMutation,
|
|
})
|
|
toast.success(includeVideo && !state.allDay ? "Événement et visio créés" : "Événement créé")
|
|
onClose()
|
|
} catch {
|
|
toast.error("Impossible de créer l'événement")
|
|
}
|
|
}
|
|
|
|
const showVideo = !state.allDay && defaultVideoProvider !== "none"
|
|
const guestTab = state.allDay ? 3 : 5
|
|
const videoTab = state.allDay ? 4 : 6
|
|
const calendarTab = state.allDay ? (showVideo ? 5 : 4) : showVideo ? 7 : 6
|
|
|
|
return (
|
|
<AgendaFloatingCard anchor={state.anchor} onClose={onClose} width={420}>
|
|
<div className="flex flex-col gap-4 px-5 pb-5 pt-3">
|
|
<div className="flex items-start gap-1">
|
|
<Input
|
|
ref={titleRef}
|
|
value={title}
|
|
tabIndex={1}
|
|
autoFocus
|
|
placeholder="Ajouter un titre"
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Enter") void save()
|
|
}}
|
|
className="h-11 min-w-0 flex-1 rounded-none border-0 border-b-2 border-border/60 !bg-transparent px-1 !text-lg shadow-none focus-visible:border-primary focus-visible:ring-0"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
tabIndex={9}
|
|
className="size-8 shrink-0 rounded-full text-muted-foreground"
|
|
aria-label="Fermer"
|
|
onClick={onClose}
|
|
>
|
|
<X className="size-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
{state.allDay ? (
|
|
<AgendaEventScheduleFields
|
|
compact
|
|
start={start}
|
|
end={end}
|
|
allDay
|
|
stepMinutes={buttonSnapMinutes}
|
|
tabIndexBase={2}
|
|
onChange={(nextStart, nextEnd) => {
|
|
setStart(nextStart)
|
|
setEnd(nextEnd)
|
|
}}
|
|
/>
|
|
) : (
|
|
<AgendaEventScheduleFields
|
|
compact
|
|
start={start}
|
|
end={end}
|
|
allDay={false}
|
|
stepMinutes={buttonSnapMinutes}
|
|
tabIndexBase={2}
|
|
onChange={(nextStart, nextEnd) => {
|
|
setStart(nextStart)
|
|
setEnd(nextEnd)
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-start gap-2">
|
|
<Users className="mt-2 size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
<div className="min-w-0 flex-1">
|
|
<AgendaGuestPicker
|
|
attendees={attendees}
|
|
onChange={setAttendees}
|
|
organizerEmail={userEmail}
|
|
tabIndex={guestTab}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{showVideo ? (
|
|
<div className="flex items-start gap-2">
|
|
<Video className="mt-2 size-4 shrink-0 text-muted-foreground" aria-hidden />
|
|
<AgendaVideoToggle
|
|
compact
|
|
provider={defaultVideoProvider}
|
|
enabled={includeVideo}
|
|
onEnabledChange={setIncludeVideo}
|
|
pending={createMutation.isPending || meetLinkMutation.isPending}
|
|
tabIndex={videoTab}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
|
|
<Select value={calendarId} onValueChange={setCalendarId}>
|
|
<SelectTrigger
|
|
tabIndex={calendarTab}
|
|
className="h-9 w-fit min-w-44 border-0 bg-muted/60 shadow-none"
|
|
>
|
|
<SelectValue placeholder="Agenda" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{calendars.map((cal) => (
|
|
<SelectItem key={cal.id} value={cal.id}>
|
|
<span className="flex items-center gap-2">
|
|
<span
|
|
className="size-3 rounded-full"
|
|
style={{ backgroundColor: calendarColor(cal) }}
|
|
/>
|
|
{cal.display_name}
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
tabIndex={7}
|
|
className="rounded-full"
|
|
onClick={() => onMoreOptions({ ...draft, includeVideo })}
|
|
>
|
|
Autres options
|
|
</Button>
|
|
<Button
|
|
tabIndex={8}
|
|
className="rounded-full px-5"
|
|
disabled={
|
|
createMutation.isPending ||
|
|
meetLinkMutation.isPending ||
|
|
!calendarId
|
|
}
|
|
onClick={() => void save()}
|
|
>
|
|
Enregistrer
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</AgendaFloatingCard>
|
|
)
|
|
}
|