Files
webmail-client/frontend/src/components/compose/SaveTemplateDialog.tsx
T
lukasandClaude Opus 5.5 8a1b9979e9 Productivity features, UI translations, local email translation
- Snooze (with a Snoozed folder), keyboard shortcuts with a Settings
  section, mail templates (Compose > More > Templates), a Subscriptions
  folder with bulk unsubscribe/delete, and tracker blocking with a
  Privacy toggle (on by default)
- Own dialogs instead of browser popups; emoji picker in compose; compose
  windows survive a visit to Settings
- Sentence completion learned from the user's sent mail (no AI)
- Interface in English and Polish (one file per language, English as the
  fallback), picked from the browser on first run, then kept
- Email translation on the server via LibreTranslate: Translate button
  and "Translate to...", messages in several languages translated part
  by part, names/addresses/emoji/file names/phone numbers kept, an
  animated line while translating
- Antispam "Mark as spam" loads with the message, not when the menu opens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:00:31 +02:00

70 lines
2.4 KiB
TypeScript

import { useState } from "react";
import { useT } from "../../i18n";
import { ApiError } from "../../lib/api";
import type { MailTemplate } from "../../lib/types";
import { AppDialog, DialogButtons } from "../AppDialog";
/** Compose → More → Templates → Save as template: name it, save it. A name
* already taken turns the button into "Replace" rather than asking again. */
export function SaveTemplateDialog({
initialName,
existing,
onSave,
onClose,
}: {
initialName: string;
existing: MailTemplate[];
onSave: (name: string) => Promise<void>;
onClose: () => void;
}) {
const t = useT();
const [name, setName] = useState(initialName);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const trimmed = name.trim();
const taken = existing.some((x) => x.name.toLowerCase() === trimmed.toLowerCase());
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!trimmed || saving) return;
setSaving(true);
setError("");
try {
await onSave(trimmed);
onClose();
} catch (err) {
setError(err instanceof ApiError ? err.message : t("compose.templateSaveFailed"));
setSaving(false);
}
};
return (
<AppDialog title={t("compose.saveTemplateTitle")} onClose={onClose}>
<form onSubmit={submit}>
<p className="mt-2 text-sm text-app-muted">{t("compose.saveTemplateHint")}</p>
<label className="mt-4 block text-sm">
<span className="text-app-muted">{t("compose.templateName")}</span>
<input
className="mt-1 w-full rounded-lg border border-app-border bg-app-bg px-3 py-1.5 text-app-fg placeholder:text-app-faint focus:border-app-accent focus:outline-none"
value={name}
maxLength={80}
placeholder={t("compose.templateNamePlaceholder")}
onChange={(e) => setName(e.target.value)}
autoFocus
onFocus={(e) => e.currentTarget.select()}
/>
</label>
{taken && <p className="mt-2 text-xs text-amber-600 dark:text-amber-400">{t("compose.templateReplaceNote")}</p>}
{error && <p className="mt-2 text-xs text-red-500">{error}</p>}
<DialogButtons
submit
confirmLabel={saving ? t("compose.templateSaving") : taken ? t("compose.templateReplaceAction") : t("common.save")}
disabled={!trimmed || saving}
onCancel={onClose}
/>
</form>
</AppDialog>
);
}