Files
Nexus/apps/web/src/components/admin/system-settings/SmtpSettingsPanel.tsx
T
Hartmut c9be7c9bbf refactor(web): make SmtpSettingsPanel self-contained, eliminating prop drilling
SmtpSettingsPanel now owns its form state, save/test mutations, and feedback state
internally. Props reduced from 17 to 2 (initialSettings + onSettingsSaved callback).
Removes 7 useState declarations, 2 mutation definitions, and 1 handler from the parent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 23:20:36 +02:00

202 lines
6.6 KiB
TypeScript

import { useState, useEffect } from "react";
import { trpc } from "~/lib/trpc/client.js";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import {
CHECKBOX_ROW_CLASS,
INPUT_CLASS,
LABEL_CLASS,
PANEL_CLASS,
PRIMARY_BUTTON_CLASS,
SECONDARY_BUTTON_CLASS,
RuntimeSecretCard,
type RuntimeSecrets,
type SaveResult,
} from "./shared.js";
type SmtpSettingsPanelProps = {
initialSettings: {
smtpHost: string | null;
smtpPort: number | null;
smtpUser: string | null;
smtpFrom: string | null;
smtpTls: boolean | null;
runtimeSecrets: { smtpPassword: RuntimeSecrets["smtpPassword"] };
};
onSettingsSaved: () => void;
};
export function SmtpSettingsPanel({ initialSettings, onSettingsSaved }: SmtpSettingsPanelProps) {
const [smtpHost, setSmtpHost] = useState("");
const [smtpPort, setSmtpPort] = useState(587);
const [smtpUser, setSmtpUser] = useState("");
const [smtpFrom, setSmtpFrom] = useState("");
const [smtpTls, setSmtpTls] = useState(true);
const [saved, setSaved] = useState(false);
const [testResult, setTestResult] = useState<SaveResult | null>(null);
useEffect(() => {
setSmtpHost(initialSettings.smtpHost ?? "");
setSmtpPort(initialSettings.smtpPort ?? 587);
setSmtpUser(initialSettings.smtpUser ?? "");
setSmtpFrom(initialSettings.smtpFrom ?? "");
setSmtpTls(initialSettings.smtpTls ?? true);
}, [initialSettings]);
const saveMutation = trpc.settings.updateSystemSettings.useMutation({
onSuccess: () => {
setSaved(true);
setTestResult(null);
onSettingsSaved();
setTimeout(() => setSaved(false), 3000);
},
});
const testMutation = trpc.settings.testSmtpConnection.useMutation({
onSuccess: (data) => setTestResult(data),
onError: (error) => setTestResult({ ok: false, error: error.message }),
});
function handleSave() {
saveMutation.mutate({
smtpHost: smtpHost || undefined,
smtpPort,
smtpUser: smtpUser || undefined,
smtpFrom: smtpFrom || undefined,
smtpTls,
});
}
return (
<div className={PANEL_CLASS}>
<div>
<h2 className="flex items-center text-base font-semibold text-gray-900 dark:text-gray-100">
Email Notifications (SMTP){" "}
<InfoTooltip content="Configure SMTP to send email notifications for vacation approvals and rejections." />
</h2>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Used to send email notifications when vacation requests are approved or rejected.
</p>
</div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label className={LABEL_CLASS}>
<span className="flex items-center">
SMTP Host{" "}
<InfoTooltip content="The SMTP server hostname, for example smtp.gmail.com or smtp.office365.com." />
</span>
</label>
<input
type="text"
className={INPUT_CLASS}
value={smtpHost}
onChange={(event) => setSmtpHost(event.target.value)}
placeholder="smtp.example.com"
/>
</div>
<div>
<label className={LABEL_CLASS}>
<span className="flex items-center">
SMTP Port{" "}
<InfoTooltip content="Common ports: 587 for STARTTLS, 465 for SSL/TLS, 25 for unencrypted." />
</span>
</label>
<input
type="number"
className={INPUT_CLASS}
value={smtpPort}
onChange={(event) => setSmtpPort(parseInt(event.target.value, 10))}
min={1}
max={65535}
/>
</div>
<div>
<label className={LABEL_CLASS}>
<span className="flex items-center">
SMTP Username <InfoTooltip content="Authentication username for the SMTP server." />
</span>
</label>
<input
type="text"
className={INPUT_CLASS}
value={smtpUser}
onChange={(event) => setSmtpUser(event.target.value)}
placeholder="user@example.com"
autoComplete="off"
/>
</div>
<div>
<label className={LABEL_CLASS}>
<span className="flex items-center">
From Address{" "}
<InfoTooltip content="The sender email address shown in notification emails." />
</span>
</label>
<input
type="email"
className={INPUT_CLASS}
value={smtpFrom}
onChange={(event) => setSmtpFrom(event.target.value)}
placeholder="noreply@capakraken.app"
/>
</div>
<div className={`${CHECKBOX_ROW_CLASS} pt-0 md:mt-[1.65rem]`}>
<input
type="checkbox"
id="smtpTls"
checked={smtpTls}
onChange={(event) => setSmtpTls(event.target.checked)}
className="rounded border-gray-300 text-brand-600"
/>
<label
htmlFor="smtpTls"
className="cursor-pointer text-sm text-gray-700 dark:text-gray-300"
>
Use TLS
</label>
</div>
</div>
<RuntimeSecretCard
title="SMTP Password"
description="SMTP credentials are provisioned outside the application and injected at runtime."
secret={initialSettings.runtimeSecrets.smtpPassword}
optionalNote="Provision SMTP_PASSWORD in the deployment target used by the API service."
/>
<div className="flex items-center gap-3">
<button
type="button"
onClick={handleSave}
disabled={saveMutation.isPending}
className={PRIMARY_BUTTON_CLASS}
>
{saveMutation.isPending ? "Saving\u2026" : "Save SMTP Settings"}
</button>
<button
type="button"
onClick={() => testMutation.mutate()}
disabled={testMutation.isPending}
className={SECONDARY_BUTTON_CLASS}
>
{testMutation.isPending ? "Testing\u2026" : "Test Connection"}
</button>
{saved ? (
<span className="text-sm font-medium text-green-600 dark:text-green-400">Saved!</span>
) : null}
{testResult ? (
<span
className={`text-sm font-medium ${
testResult.ok
? "text-green-600 dark:text-green-400"
: "text-red-500 dark:text-red-400"
}`}
>
{testResult.ok ? "\u2713 Connection successful" : `\u2717 ${testResult.error}`}
</span>
) : null}
</div>
</div>
);
}