185 lines
7.8 KiB
TypeScript
185 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
type Status = {
|
|
smtp: { host: string; port: number; user: string; from: string; passGesetzt: boolean };
|
|
telegram: { botTokenGesetzt: boolean };
|
|
logo: { secretGesetzt: boolean; publishableKey: string };
|
|
verschluesselungBereit: boolean;
|
|
};
|
|
|
|
const inputClass =
|
|
"mt-1.5 h-11 w-full rounded-field border border-line bg-white px-3.5 text-sm text-ink placeholder:text-muted/60";
|
|
|
|
async function saveSection(section: string, values: Record<string, string>) {
|
|
const res = await fetch("/api/settings/integrations", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ section, values }),
|
|
});
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data.message ?? `Fehler: ${(data.errors ?? []).join(", ")}`);
|
|
return data.status as Status;
|
|
}
|
|
|
|
function Section({
|
|
title,
|
|
children,
|
|
}: {
|
|
title: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<section className="rounded-card border border-line bg-white p-5">
|
|
<h2 className="text-[15px] font-semibold tracking-tight">{title}</h2>
|
|
<div className="mt-3">{children}</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function SecretHint({ gesetzt }: { gesetzt: boolean }) {
|
|
return (
|
|
<p className="mt-1 text-[13px] text-muted">
|
|
{gesetzt ? "Gespeichert. Leer lassen, um den Wert zu behalten." : "Noch nicht gespeichert."}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
export default function IntegrationsForm() {
|
|
const [status, setStatus] = useState<Status | null>(null);
|
|
const [loadError, setLoadError] = useState("");
|
|
const [notice, setNotice] = useState("");
|
|
const [smtp, setSmtp] = useState({ host: "", port: "587", user: "", pass: "", from: "" });
|
|
const [telegramToken, setTelegramToken] = useState("");
|
|
const [logoSecret, setLogoSecret] = useState("");
|
|
const [logoPublic, setLogoPublic] = useState("");
|
|
|
|
useEffect(() => {
|
|
fetch("/api/settings/integrations")
|
|
.then((r) => (r.ok ? r.json() : Promise.reject(new Error("laden fehlgeschlagen"))))
|
|
.then((s: Status) => {
|
|
setStatus(s);
|
|
setSmtp({ host: s.smtp.host, port: String(s.smtp.port), user: s.smtp.user, pass: "", from: s.smtp.from });
|
|
setLogoPublic(s.logo.publishableKey);
|
|
})
|
|
.catch(() => setLoadError("Einstellungen konnten nicht geladen werden."));
|
|
}, []);
|
|
|
|
async function onSave(section: "smtp" | "telegram" | "logo", values: Record<string, string>, okText: string) {
|
|
setNotice("");
|
|
try {
|
|
const s = await saveSection(section, values);
|
|
setStatus(s);
|
|
if (section === "smtp") setSmtp((v) => ({ ...v, pass: "" }));
|
|
if (section === "telegram") setTelegramToken("");
|
|
if (section === "logo") {
|
|
setLogoSecret("");
|
|
setLogoPublic(s.logo.publishableKey);
|
|
}
|
|
setNotice(okText);
|
|
} catch (e) {
|
|
setNotice(e instanceof Error ? e.message : "Speichern fehlgeschlagen");
|
|
}
|
|
}
|
|
|
|
if (loadError) return <p role="alert" className="text-sm font-medium text-danger">{loadError}</p>;
|
|
if (!status) return <p className="text-sm text-muted">Wird geladen …</p>;
|
|
|
|
return (
|
|
<div className="grid gap-4">
|
|
<Section title="E-Mail (SMTP)">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="sm:col-span-2">
|
|
<label htmlFor="smtp-host" className="block text-[13px] font-semibold">Server</label>
|
|
<input id="smtp-host" value={smtp.host} onChange={(e) => setSmtp({ ...smtp, host: e.target.value })} placeholder="mail.beispiel.de" autoComplete="off" className={inputClass} />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="smtp-port" className="block text-[13px] font-semibold">Port</label>
|
|
<input id="smtp-port" inputMode="numeric" value={smtp.port} onChange={(e) => setSmtp({ ...smtp, port: e.target.value })} placeholder="587" autoComplete="off" className={inputClass} />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="smtp-user" className="block text-[13px] font-semibold">Benutzername</label>
|
|
<input id="smtp-user" value={smtp.user} onChange={(e) => setSmtp({ ...smtp, user: e.target.value })} autoComplete="username" className={inputClass} />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="smtp-pass" className="block text-[13px] font-semibold">Passwort</label>
|
|
<input id="smtp-pass" type="password" value={smtp.pass} onChange={(e) => setSmtp({ ...smtp, pass: e.target.value })} autoComplete="new-password" placeholder={status.smtp.passGesetzt ? "••••••••" : ""} className={inputClass} />
|
|
<SecretHint gesetzt={status.smtp.passGesetzt} />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="smtp-from" className="block text-[13px] font-semibold">Absender</label>
|
|
<input id="smtp-from" type="email" value={smtp.from} onChange={(e) => setSmtp({ ...smtp, from: e.target.value })} placeholder="gutscheine@beispiel.de" autoComplete="email" className={inputClass} />
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => onSave("smtp", { smtp_host: smtp.host, smtp_port: smtp.port, smtp_user: smtp.user, smtp_pass: smtp.pass, mail_from: smtp.from }, "E-Mail-Einstellungen gespeichert.")}
|
|
className="mt-4 h-11 rounded-field bg-brand-600 px-6 text-sm font-semibold text-white transition-colors hover:bg-brand-700 active:scale-[0.99]"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</Section>
|
|
|
|
<Section title="Telegram">
|
|
<label htmlFor="tg-token" className="block text-[13px] font-semibold">Bot-Token</label>
|
|
<input
|
|
id="tg-token"
|
|
type="password"
|
|
value={telegramToken}
|
|
onChange={(e) => setTelegramToken(e.target.value)}
|
|
autoComplete="off"
|
|
placeholder={status.telegram.botTokenGesetzt ? "••••••••" : "123456:ABC-DEF…"}
|
|
className={inputClass}
|
|
/>
|
|
<SecretHint gesetzt={status.telegram.botTokenGesetzt} />
|
|
<button
|
|
type="button"
|
|
onClick={() => onSave("telegram", { telegram_bot_token: telegramToken }, "Telegram-Token gespeichert.")}
|
|
className="mt-4 h-11 rounded-field bg-brand-600 px-6 text-sm font-semibold text-white transition-colors hover:bg-brand-700 active:scale-[0.99]"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</Section>
|
|
|
|
<Section title="Anbieterlogos (Logo.dev)">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label htmlFor="logo-secret" className="block text-[13px] font-semibold">Secret-Key (Suche)</label>
|
|
<input
|
|
id="logo-secret"
|
|
type="password"
|
|
value={logoSecret}
|
|
onChange={(e) => setLogoSecret(e.target.value)}
|
|
autoComplete="off"
|
|
placeholder={status.logo.secretGesetzt ? "••••••••" : "sk_…"}
|
|
className={inputClass}
|
|
/>
|
|
<SecretHint gesetzt={status.logo.secretGesetzt} />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="logo-public" className="block text-[13px] font-semibold">Public-Key (Anzeige)</label>
|
|
<input
|
|
id="logo-public"
|
|
value={logoPublic}
|
|
onChange={(e) => setLogoPublic(e.target.value)}
|
|
autoComplete="off"
|
|
placeholder="pk_…"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => onSave("logo", { logo_dev_secret_key: logoSecret, logo_dev_publishable_key: logoPublic }, "Logo.dev-Schlüssel gespeichert.")}
|
|
className="mt-4 h-11 rounded-field bg-brand-600 px-6 text-sm font-semibold text-white transition-colors hover:bg-brand-700 active:scale-[0.99]"
|
|
>
|
|
Speichern
|
|
</button>
|
|
</Section>
|
|
|
|
{notice && <p role="status" className="text-sm font-medium text-muted">{notice}</p>}
|
|
</div>
|
|
);
|
|
}
|