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.
345 lines
12 KiB
TypeScript
345 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useMemo, useState } from "react"
|
|
import { toast } from "sonner"
|
|
import { MoreVertical, Pencil, Plus, Trash2 } from "lucide-react"
|
|
import { AgendaCalendarDialog } from "@/components/agenda/agenda-calendar-dialog"
|
|
import {
|
|
CalendarViewDialog,
|
|
ExternalCalendarDialog,
|
|
} from "@/components/agenda/agenda-calendars-settings"
|
|
import { AgendaMiniMonth } from "@/components/agenda/agenda-mini-month"
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
import { useDeleteAgendaCalendar } from "@/lib/api/hooks/use-calendar-mutations"
|
|
import { useLabels } from "@/lib/api/hooks/use-folder-label-queries"
|
|
import { useMailAccounts } from "@/lib/api/hooks/use-mail-queries"
|
|
import { ALL_AGENDAS_VIEW_LABEL, isExternalCalendarId, isReservedAgendaViewName } from "@/lib/agenda/agenda-calendar-visibility"
|
|
import { calendarColor } from "@/lib/agenda/agenda-events"
|
|
import { useAgendaSettingsStore, useAgendaUIStore } from "@/lib/agenda/agenda-store"
|
|
import type { AgendaCalendar } from "@/lib/agenda/agenda-types"
|
|
import { useMergedAgendaCalendars } from "@/lib/agenda/use-visible-agenda-calendars"
|
|
import { useIsMobile } from "@/hooks/use-mobile"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
export function AgendaSidebar({
|
|
selectedDate,
|
|
onSelectDate,
|
|
onCreateEvent,
|
|
}: {
|
|
selectedDate: Date
|
|
onSelectDate: (date: Date) => void
|
|
onCreateEvent: () => void
|
|
}) {
|
|
const isMobile = useIsMobile()
|
|
const sidebarCollapsed = useAgendaUIStore((s) => s.sidebarCollapsed)
|
|
const setSidebarCollapsed = useAgendaUIStore((s) => s.setSidebarCollapsed)
|
|
const hiddenIds = useAgendaSettingsStore((s) => s.hiddenCalendarIds)
|
|
const weekStart = useAgendaSettingsStore((s) => s.weekStart)
|
|
const calendarViews = useAgendaSettingsStore((s) => s.calendarViews)
|
|
const activeCalendarViewId = useAgendaSettingsStore((s) => s.activeCalendarViewId)
|
|
const setActiveCalendarViewId = useAgendaSettingsStore((s) => s.setActiveCalendarViewId)
|
|
const toggleCalendar = useAgendaSettingsStore((s) => s.toggleCalendarVisible)
|
|
const { data: accounts = [] } = useMailAccounts()
|
|
const { data: labels = [] } = useLabels()
|
|
const { calendars, isLoading } = useMergedAgendaCalendars()
|
|
const deleteMutation = useDeleteAgendaCalendar()
|
|
|
|
const [calendarDialogOpen, setCalendarDialogOpen] = useState(false)
|
|
const [editingCalendar, setEditingCalendar] = useState<AgendaCalendar | null>(null)
|
|
const [deletingCalendar, setDeletingCalendar] = useState<AgendaCalendar | null>(null)
|
|
const [externalDialogOpen, setExternalDialogOpen] = useState(false)
|
|
const [viewDialogOpen, setViewDialogOpen] = useState(false)
|
|
|
|
const calendarOptions = useMemo(
|
|
() =>
|
|
(calendars ?? []).map((calendar) => ({
|
|
id: calendar.id,
|
|
label: calendar.display_name,
|
|
color: calendarColor(calendar),
|
|
})),
|
|
[calendars],
|
|
)
|
|
|
|
const labelOptions = useMemo(
|
|
() => labels.map((label) => ({ id: label.id, label: label.name })),
|
|
[labels],
|
|
)
|
|
|
|
const open = !sidebarCollapsed
|
|
|
|
const confirmDelete = async () => {
|
|
if (!deletingCalendar) return
|
|
try {
|
|
await deleteMutation.mutateAsync({ id: deletingCalendar.id })
|
|
toast.success(`Agenda « ${deletingCalendar.display_name} » supprimé`)
|
|
} catch {
|
|
toast.error("Impossible de supprimer cet agenda")
|
|
} finally {
|
|
setDeletingCalendar(null)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<aside
|
|
className={cn(
|
|
"flex h-full w-64 shrink-0 flex-col gap-4 overflow-y-auto bg-app-canvas px-3 pt-3 pb-4",
|
|
isMobile
|
|
? cn(
|
|
"fixed inset-y-0 left-0 z-50 shadow-xl transition-transform duration-200 ease-linear",
|
|
open ? "translate-x-0" : "-translate-x-full pointer-events-none",
|
|
)
|
|
: cn(!open && "hidden"),
|
|
)}
|
|
aria-hidden={isMobile && !open}
|
|
>
|
|
<Button
|
|
className="h-13 w-fit gap-3 rounded-2xl border border-border/60 bg-card px-5 text-[0.95rem] font-medium text-foreground shadow-md hover:bg-mail-nav-hover hover:shadow-lg"
|
|
variant="ghost"
|
|
onClick={() => {
|
|
onCreateEvent()
|
|
if (isMobile) setSidebarCollapsed(true)
|
|
}}
|
|
>
|
|
<Plus className="size-6 text-primary" />
|
|
Nouvel événement
|
|
</Button>
|
|
|
|
<AgendaMiniMonth
|
|
selected={selectedDate}
|
|
weekStart={weekStart}
|
|
onSelect={(d) => {
|
|
onSelectDate(d)
|
|
if (isMobile) setSidebarCollapsed(true)
|
|
}}
|
|
/>
|
|
|
|
<div className="flex min-h-0 flex-col gap-0.5">
|
|
<div className="flex items-center justify-between pr-1 pl-2">
|
|
<span className="py-1 text-sm font-medium text-foreground/90">Vues</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-7 rounded-full text-muted-foreground"
|
|
aria-label="Créer une vue"
|
|
onClick={() => setViewDialogOpen(true)}
|
|
>
|
|
<Plus className="size-4" />
|
|
</Button>
|
|
</div>
|
|
{calendarViews.length > 0 ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveCalendarViewId(null)}
|
|
className={cn(
|
|
"rounded-lg px-2 py-1 text-left text-sm transition-colors hover:bg-mail-nav-hover",
|
|
activeCalendarViewId === null
|
|
? "bg-mail-nav-selected font-medium text-foreground"
|
|
: "text-foreground/85",
|
|
)}
|
|
>
|
|
{ALL_AGENDAS_VIEW_LABEL}
|
|
</button>
|
|
{calendarViews
|
|
.filter((view) => !isReservedAgendaViewName(view.name))
|
|
.map((view) => (
|
|
<button
|
|
key={view.id}
|
|
type="button"
|
|
onClick={() => setActiveCalendarViewId(view.id)}
|
|
className={cn(
|
|
"rounded-lg px-2 py-1 text-left text-sm transition-colors hover:bg-mail-nav-hover",
|
|
activeCalendarViewId === view.id
|
|
? "bg-mail-nav-selected font-medium text-foreground"
|
|
: "text-foreground/85",
|
|
)}
|
|
>
|
|
{view.name}
|
|
</button>
|
|
))}
|
|
</>
|
|
) : null}
|
|
|
|
<div className="mt-2 flex items-center justify-between border-t border-border/60 pr-1 pl-2 pt-2">
|
|
<span className="py-0.5 text-sm font-medium text-foreground/90">
|
|
Mes agendas
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-7 rounded-full text-muted-foreground"
|
|
aria-label="Créer un agenda"
|
|
onClick={() => {
|
|
setEditingCalendar(null)
|
|
setCalendarDialogOpen(true)
|
|
}}
|
|
>
|
|
<Plus className="size-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{isLoading && (
|
|
<div className="flex flex-col gap-1.5 px-2 py-0.5">
|
|
<Skeleton className="h-5 w-40" />
|
|
<Skeleton className="h-5 w-32" />
|
|
</div>
|
|
)}
|
|
|
|
{(calendars ?? []).map((cal) => {
|
|
const color = calendarColor(cal)
|
|
const visible = !hiddenIds.includes(cal.id)
|
|
const isExternal = isExternalCalendarId(cal.id)
|
|
return (
|
|
<div
|
|
key={cal.id}
|
|
className="group flex items-center gap-2 rounded-lg px-2 py-0.5 hover:bg-mail-nav-hover"
|
|
>
|
|
<label className="flex min-w-0 flex-1 cursor-pointer items-center gap-2.5">
|
|
<input
|
|
type="checkbox"
|
|
checked={visible}
|
|
onChange={() => toggleCalendar(cal.id)}
|
|
className="peer sr-only"
|
|
/>
|
|
<span
|
|
aria-hidden
|
|
className={cn(
|
|
"flex size-4.5 shrink-0 items-center justify-center rounded-[5px] border-2 transition-colors",
|
|
)}
|
|
style={{
|
|
borderColor: color,
|
|
backgroundColor: visible ? color : "transparent",
|
|
}}
|
|
>
|
|
{visible && (
|
|
<svg viewBox="0 0 24 24" className="size-3.5 text-white" fill="none">
|
|
<path
|
|
d="M5 13l4 4L19 7"
|
|
stroke="currentColor"
|
|
strokeWidth="3"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
)}
|
|
</span>
|
|
<span className="truncate text-sm text-foreground/85">
|
|
{cal.display_name}
|
|
{isExternal ? (
|
|
<span className="ml-1 text-[10px] text-muted-foreground">(iCal)</span>
|
|
) : null}
|
|
</span>
|
|
</label>
|
|
{!isExternal ? (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-7 shrink-0 rounded-full text-muted-foreground opacity-0 group-hover:opacity-100 data-[state=open]:opacity-100"
|
|
aria-label={`Options de ${cal.display_name}`}
|
|
>
|
|
<MoreVertical className="size-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="start" className="w-44">
|
|
<DropdownMenuItem
|
|
onSelect={() => {
|
|
setEditingCalendar(cal)
|
|
setCalendarDialogOpen(true)
|
|
}}
|
|
>
|
|
<Pencil className="size-4" /> Modifier
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
variant="destructive"
|
|
onSelect={() => setDeletingCalendar(cal)}
|
|
>
|
|
<Trash2 className="size-4" /> Supprimer
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
) : null}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</aside>
|
|
|
|
<AgendaCalendarDialog
|
|
open={calendarDialogOpen}
|
|
onOpenChange={setCalendarDialogOpen}
|
|
calendar={editingCalendar}
|
|
onAddExternalICal={
|
|
editingCalendar
|
|
? undefined
|
|
: () => {
|
|
setCalendarDialogOpen(false)
|
|
setExternalDialogOpen(true)
|
|
}
|
|
}
|
|
/>
|
|
|
|
<ExternalCalendarDialog
|
|
open={externalDialogOpen}
|
|
onOpenChange={setExternalDialogOpen}
|
|
calendar={null}
|
|
accounts={accounts}
|
|
/>
|
|
|
|
<CalendarViewDialog
|
|
open={viewDialogOpen}
|
|
onOpenChange={setViewDialogOpen}
|
|
view={null}
|
|
calendarOptions={calendarOptions}
|
|
labelOptions={labelOptions}
|
|
/>
|
|
|
|
<AlertDialog
|
|
open={deletingCalendar !== null}
|
|
onOpenChange={(o) => {
|
|
if (!o) setDeletingCalendar(null)
|
|
}}
|
|
>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>
|
|
Supprimer « {deletingCalendar?.display_name} » ?
|
|
</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Tous les événements de cet agenda seront définitivement supprimés.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Annuler</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="bg-destructive text-white hover:bg-destructive/90"
|
|
onClick={() => void confirmDelete()}
|
|
>
|
|
Supprimer
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
)
|
|
}
|