163 lines
6.6 KiB
TypeScript
163 lines
6.6 KiB
TypeScript
"use client";
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import Shell from "../components/shell";
|
|
import { validateContact } from "@/lib/contacts";
|
|
|
|
type Contact = { id: number; name: string; email: string | null; telegramChatId: string | null; householdId: number | null };
|
|
|
|
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";
|
|
|
|
export default function ContactsPage() {
|
|
const [contacts, setContacts] = useState<Contact[]>([]);
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [telegramChatId, setTelegramChatId] = useState("");
|
|
const [errors, setErrors] = useState<string[]>([]);
|
|
const [scope, setScope] = useState<"private" | "household">("private");
|
|
const [householdName, setHouseholdName] = useState<string | null>(null);
|
|
|
|
async function load() {
|
|
const res = await fetch("/api/contacts");
|
|
if (res.ok) setContacts(await res.json());
|
|
}
|
|
useEffect(() => {
|
|
load();
|
|
fetch("/api/users/me")
|
|
.then((r) => (r.ok ? r.json() : null))
|
|
.then((me) => setHouseholdName(me?.householdName ?? null))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
async function onSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
const input = { name, email: email || null, telegramChatId: telegramChatId || null };
|
|
const errs = validateContact(input);
|
|
setErrors(errs);
|
|
if (errs.length) return;
|
|
const res = await fetch("/api/contacts", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ ...input, scope }),
|
|
});
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}));
|
|
setErrors(data.errors ?? ["speichern"]);
|
|
return;
|
|
}
|
|
setName(""); setEmail(""); setTelegramChatId(""); setErrors([]);
|
|
load();
|
|
}
|
|
|
|
return (
|
|
<Shell>
|
|
<Link href="/" className="text-sm font-medium text-muted hover:text-ink">
|
|
← Zurück
|
|
</Link>
|
|
<h1 className="mt-2 text-2xl font-semibold tracking-tight">Kontakte</h1>
|
|
<p className="mt-1 text-sm text-muted">Mindestens E-Mail oder Telegram pro Kontakt.</p>
|
|
<form onSubmit={onSubmit} className="mt-5 rounded-card border border-line bg-white p-5 sm:p-6" noValidate>
|
|
<fieldset className="mb-4">
|
|
<legend className="text-[13px] font-semibold">Wo soll der Kontakt gespeichert werden?</legend>
|
|
<div className="mt-2 flex gap-2" role="radiogroup" aria-label="Bestand">
|
|
<button
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={scope === "private"}
|
|
onClick={() => setScope("private")}
|
|
className={`h-11 flex-1 rounded-field border text-sm font-semibold transition-colors sm:flex-none sm:px-8 ${
|
|
scope === "private" ? "border-brand-700 bg-brand-600 text-white" : "border-line bg-white hover:border-brand-600"
|
|
}`}
|
|
>
|
|
Privat
|
|
</button>
|
|
{householdName && (
|
|
<button
|
|
type="button"
|
|
role="radio"
|
|
aria-checked={scope === "household"}
|
|
onClick={() => setScope("household")}
|
|
className={`h-11 flex-1 rounded-field border text-sm font-semibold transition-colors sm:flex-none sm:px-8 ${
|
|
scope === "household" ? "border-brand-700 bg-brand-600 text-white" : "border-line bg-white hover:border-brand-600"
|
|
}`}
|
|
>
|
|
{householdName}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</fieldset>
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="sm:col-span-2">
|
|
<label htmlFor="name" className="block text-[13px] font-semibold">Name</label>
|
|
<input
|
|
id="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
autoComplete="off"
|
|
placeholder="z. B. Mama"
|
|
aria-invalid={errors.includes("name")}
|
|
className={`${inputClass} ${errors.includes("name") ? "border-danger" : ""}`}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="email" className="block text-[13px] font-semibold">E-Mail</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
autoComplete="email"
|
|
placeholder="name@beispiel.de"
|
|
aria-invalid={errors.includes("email")}
|
|
className={`${inputClass} ${errors.includes("email") ? "border-danger" : ""}`}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="telegramChatId" className="block text-[13px] font-semibold">Telegram-Chat-ID</label>
|
|
<input
|
|
id="telegramChatId"
|
|
value={telegramChatId}
|
|
onChange={(e) => setTelegramChatId(e.target.value)}
|
|
autoComplete="off"
|
|
placeholder="z. B. 123456789"
|
|
className={inputClass}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{errors.length > 0 && (
|
|
<p role="alert" className="mt-4 rounded-field bg-danger/10 px-3.5 py-2.5 text-sm font-medium text-danger">
|
|
Bitte prüfe die Eingaben: {errors.join(", ")}
|
|
</p>
|
|
)}
|
|
<button
|
|
type="submit"
|
|
className="mt-5 h-12 w-full rounded-field bg-brand-600 text-sm font-semibold text-white transition-colors hover:bg-brand-700 active:scale-[0.99] sm:w-auto sm:px-8"
|
|
>
|
|
Hinzufügen
|
|
</button>
|
|
</form>
|
|
<section aria-label="Gespeicherte Kontakte" className="mt-5 rounded-card border border-line bg-white px-5 py-2">
|
|
{contacts.length === 0 ? (
|
|
<p className="py-3 text-sm text-muted">Noch keine Kontakte angelegt.</p>
|
|
) : (
|
|
<ul className="divide-y divide-line">
|
|
{contacts.map((c) => (
|
|
<li key={c.id} className="flex items-center justify-between gap-3 py-3 text-sm">
|
|
<div>
|
|
<span className="font-semibold">{c.name}</span>
|
|
{c.email && <span className="text-muted"> · {c.email}</span>}
|
|
{c.telegramChatId && <span className="text-muted"> · Telegram {c.telegramChatId}</span>}
|
|
</div>
|
|
<span className="shrink-0 rounded-full bg-wash px-2.5 py-0.5 text-xs font-medium text-muted">
|
|
{c.householdId !== null ? householdName ?? "Haushalt" : "Privat"}
|
|
</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
</Shell>
|
|
);
|
|
}
|