mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 00:13:09 +00:00
170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
"use client";
|
|
|
|
import type { ReactNode } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Trash2, Plus } from "@/components/icons";
|
|
import type { Calendar, CalendarEvent, CalendarEventAlert } from "@/lib/jmap/types";
|
|
import {
|
|
describeAlert,
|
|
getEffectiveAlerts,
|
|
newAlertRow,
|
|
type AlertAction,
|
|
type AlertRow,
|
|
type AlertUnit,
|
|
} from "@/lib/calendar-alerts";
|
|
|
|
type CalendarT = ReturnType<typeof useTranslations>;
|
|
|
|
const selectClass =
|
|
"rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring";
|
|
|
|
/** "15 minutes before", plus "by email" for email alerts. */
|
|
export function formatAlertRowLabel(row: Omit<AlertRow, "id">, t: CalendarT): string {
|
|
let label: string;
|
|
switch (row.unit) {
|
|
case "at_time": label = t("alerts.at_time"); break;
|
|
case "minutes": label = t("alerts.minutes_before", { count: row.value }); break;
|
|
case "hours": label = t("alerts.hours_before", { count: row.value }); break;
|
|
case "days": label = t("alerts.days_before", { count: row.value }); break;
|
|
case "weeks": label = t("alerts.weeks_before", { count: row.value }); break;
|
|
}
|
|
return row.action === "email" ? t("alerts.by_email", { label }) : label;
|
|
}
|
|
|
|
/** One line for an event's effective alerts, calendar defaults included. */
|
|
export function formatEventAlertsLabel(event: CalendarEvent, calendars: Calendar[], t: CalendarT): string | null {
|
|
const alerts = getEffectiveAlerts(event, calendars);
|
|
if (!alerts) return null;
|
|
const labels: string[] = [];
|
|
for (const alert of Object.values(alerts)) {
|
|
const row = describeAlert(alert);
|
|
if (row) labels.push(formatAlertRowLabel(row, t));
|
|
}
|
|
return labels.length > 0 ? labels.join(", ") : null;
|
|
}
|
|
|
|
/** Read-only list of an alert map, for alerts that come from calendar defaults. */
|
|
export function AlertSummary({ alerts }: { alerts: Record<string, CalendarEventAlert> | null }) {
|
|
const t = useTranslations("calendar");
|
|
const labels: string[] = [];
|
|
let other = 0;
|
|
for (const alert of Object.values(alerts ?? {})) {
|
|
const row = describeAlert(alert);
|
|
if (row) labels.push(formatAlertRowLabel(row, t));
|
|
else other += 1;
|
|
}
|
|
if (labels.length === 0 && other === 0) {
|
|
return <p className="text-sm text-muted-foreground">{t("alerts.none")}</p>;
|
|
}
|
|
return (
|
|
<ul className="text-sm space-y-0.5">
|
|
{labels.map((label, i) => <li key={i}>{label}</li>)}
|
|
{other > 0 && <li className="text-muted-foreground">{t("alerts.other", { count: other })}</li>}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
interface AlertListEditorProps {
|
|
rows: AlertRow[];
|
|
onChange: (rows: AlertRow[]) => void;
|
|
/** Alerts the rows can't show (absolute, after the start); saved unchanged. */
|
|
keptCount?: number;
|
|
disabled?: boolean;
|
|
/** Shown next to "Add reminder". */
|
|
extraAction?: ReactNode;
|
|
}
|
|
|
|
/**
|
|
* Edits reminders as rows of amount, unit and action (notification or email).
|
|
* Each row is an offset before the start; alerts that don't fit stay with the
|
|
* caller and are only counted here.
|
|
*/
|
|
export function AlertListEditor({ rows, onChange, keptCount = 0, disabled = false, extraAction }: AlertListEditorProps) {
|
|
const t = useTranslations("calendar");
|
|
|
|
const update = (id: string, patch: Partial<Omit<AlertRow, "id">>) => {
|
|
onChange(rows.map((r) => (r.id === id ? { ...r, ...patch } : r)));
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
{rows.length === 0 && keptCount === 0 ? (
|
|
<p className="text-sm text-muted-foreground">{t("alerts.none")}</p>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{rows.map((row) => (
|
|
<div key={row.id} className="flex items-center gap-2">
|
|
{row.unit !== "at_time" && (
|
|
<Input
|
|
type="number"
|
|
min={1}
|
|
max={999}
|
|
value={row.value}
|
|
disabled={disabled}
|
|
onChange={(e) => {
|
|
const n = parseInt(e.target.value, 10);
|
|
update(row.id, { value: Number.isFinite(n) ? Math.max(1, n) : 1 });
|
|
}}
|
|
className="w-20"
|
|
aria-label={t("alerts.amount")}
|
|
/>
|
|
)}
|
|
<select
|
|
value={row.unit}
|
|
disabled={disabled}
|
|
onChange={(e) => {
|
|
const unit = e.target.value as AlertUnit;
|
|
update(row.id, { unit, value: unit === "at_time" ? 0 : (row.value || 1) });
|
|
}}
|
|
className={`flex-1 min-w-0 ${selectClass}`}
|
|
aria-label={t("alerts.unit")}
|
|
>
|
|
<option value="at_time">{t("alerts.at_time")}</option>
|
|
<option value="minutes">{t("alerts.unit_minutes_before")}</option>
|
|
<option value="hours">{t("alerts.unit_hours_before")}</option>
|
|
<option value="days">{t("alerts.unit_days_before")}</option>
|
|
<option value="weeks">{t("alerts.unit_weeks_before")}</option>
|
|
</select>
|
|
<select
|
|
value={row.action}
|
|
disabled={disabled}
|
|
onChange={(e) => update(row.id, { action: e.target.value as AlertAction })}
|
|
className={selectClass}
|
|
aria-label={t("alerts.action")}
|
|
>
|
|
<option value="display">{t("alerts.action_display")}</option>
|
|
<option value="email">{t("alerts.action_email")}</option>
|
|
</select>
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={() => onChange(rows.filter((r) => r.id !== row.id))}
|
|
className="p-2 rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors disabled:opacity-50"
|
|
aria-label={t("alerts.remove")}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
{keptCount > 0 && (
|
|
<p className="text-xs text-muted-foreground">{t("alerts.kept", { count: keptCount })}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={() => onChange([...rows, newAlertRow(10, "minutes")])}
|
|
className="inline-flex items-center gap-1 text-sm text-primary hover:underline disabled:opacity-50"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
{t("alerts.add")}
|
|
</button>
|
|
{extraAction}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|