Full calendar support via JMAP Calendars (RFC 8984): - Event create/edit/delete with recurrence rules and reminders - Multi-day event spanning, column-based overlap layout - Locale-aware date formatting, first day of week and time format settings - Real-time updates via push notifications - ARIA accessibility, input validation, color sanitization - Keyboard shortcuts, mobile touch targets, focus trap - ICU pluralization for all 8 supported languages
110 lines
3.7 KiB
TypeScript
110 lines
3.7 KiB
TypeScript
"use client";
|
||
|
||
import { useTranslations, useFormatter } from "next-intl";
|
||
import { Button } from "@/components/ui/button";
|
||
import { ArrowLeft, ChevronLeft, ChevronRight, Plus } 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;
|
||
isMobile?: boolean;
|
||
firstDayOfWeek?: number;
|
||
}
|
||
|
||
export function CalendarToolbar({
|
||
selectedDate,
|
||
viewMode,
|
||
onNavigateBack,
|
||
onPrev,
|
||
onNext,
|
||
onToday,
|
||
onViewModeChange,
|
||
onCreateEvent,
|
||
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>
|
||
)}
|
||
|
||
<Button size="sm" onClick={onCreateEvent}>
|
||
<Plus className="w-4 h-4 mr-1" />
|
||
{!isMobile && t("events.create")}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|