Files
SRCmail/components/calendar/calendar-toolbar.tsx
T
Matthieu MALVACHEandMatthieu MALVACHE ff65693e26 feat: add calendar drag-and-drop rescheduling and iCalendar import
- Drag events in week/day views to reschedule (15-min snap intervals)
- Drag events in month view to change date (preserves time)
- Visual snap indicators with time labels during drag
- iCalendar (.ics) file import via JMAP CalendarEvent/parse
- Import modal with file upload, event preview, calendar selector
- File validation (5MB max), bulk import with progress tracking
2026-02-16 21:09:13 +01:00

119 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useTranslations, useFormatter } from "next-intl";
import { Button } from "@/components/ui/button";
import { ArrowLeft, ChevronLeft, ChevronRight, Plus, Upload } from "lucide-react";
import { addDays, startOfWeek } from "date-fns";
import { cn } from "@/lib/utils";
import type { CalendarViewMode } from "@/stores/calendar-store";
interface CalendarToolbarProps {
selectedDate: Date;
viewMode: CalendarViewMode;
onNavigateBack: () => void;
onPrev: () => void;
onNext: () => void;
onToday: () => void;
onViewModeChange: (mode: CalendarViewMode) => void;
onCreateEvent: () => void;
onImport?: () => void;
isMobile?: boolean;
firstDayOfWeek?: number;
}
export function CalendarToolbar({
selectedDate,
viewMode,
onNavigateBack,
onPrev,
onNext,
onToday,
onViewModeChange,
onCreateEvent,
onImport,
isMobile,
firstDayOfWeek = 1,
}: CalendarToolbarProps) {
const t = useTranslations("calendar");
const formatter = useFormatter();
const views: CalendarViewMode[] = ["month", "week", "day", "agenda"];
const getDateLabel = (): string => {
switch (viewMode) {
case "month":
return formatter.dateTime(selectedDate, { month: "long", year: "numeric" });
case "week": {
const ws = startOfWeek(selectedDate, { weekStartsOn: firstDayOfWeek as 0 | 1 });
const we = addDays(ws, 6);
const sameMonth = ws.getMonth() === we.getMonth();
if (sameMonth) {
return `${formatter.dateTime(ws, { month: "short", day: "numeric" })} ${formatter.dateTime(we, { day: "numeric" })}, ${we.getFullYear()}`;
}
return `${formatter.dateTime(ws, { month: "short", day: "numeric" })} ${formatter.dateTime(we, { month: "short", day: "numeric" })}, ${we.getFullYear()}`;
}
case "day":
return formatter.dateTime(selectedDate, { weekday: "long", month: "long", day: "numeric", year: "numeric" });
case "agenda":
return formatter.dateTime(selectedDate, { month: "long", year: "numeric" });
}
};
return (
<div className="flex items-center gap-2 px-4 py-3 border-b border-border flex-wrap">
<Button variant="ghost" size="sm" onClick={onNavigateBack} className="mr-1">
<ArrowLeft className="w-4 h-4 mr-1" />
{!isMobile && t("back_to_email")}
</Button>
<div className="flex items-center gap-1">
<button onClick={onPrev} className="p-1.5 rounded hover:bg-muted transition-colors" aria-label={t("nav_prev")}>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-sm font-medium min-w-[140px] text-center">
{getDateLabel()}
</span>
<button onClick={onNext} className="p-1.5 rounded hover:bg-muted transition-colors" aria-label={t("nav_next")}>
<ChevronRight className="w-4 h-4" />
</button>
</div>
<Button variant="outline" size="sm" onClick={onToday}>
{t("views.today")}
</Button>
<div className="flex-1" />
{!isMobile && (
<div className="flex border border-border rounded-md overflow-hidden">
{views.map((v) => (
<button
key={v}
onClick={() => onViewModeChange(v)}
className={cn(
"px-3 py-1.5 text-xs font-medium transition-colors",
v === viewMode
? "bg-primary text-primary-foreground"
: "hover:bg-muted text-muted-foreground"
)}
>
{t(`views.${v}`)}
</button>
))}
</div>
)}
{onImport && (
<Button variant="outline" size="sm" onClick={onImport}>
<Upload className="w-4 h-4 mr-1" />
{!isMobile && t("import.title")}
</Button>
)}
<Button size="sm" onClick={onCreateEvent}>
<Plus className="w-4 h-4 mr-1" />
{!isMobile && t("events.create")}
</Button>
</div>
);
}