since3-portal/modules/shared/ui/components/credential-dialog.tsx
2026-09-28 20:17:33 +02:00

167 lines
6.2 KiB
TypeScript

"use client"
import { useActionState, useState } from "react";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";
import { createCredential, updateCredential } from "@/modules/shared/actions/credentials";
import { SERVICES } from "@/modules/shared/data/services-data";
import { ServiceLogo } from "@/modules/shared/ui/components/service-logo";
type Credential = {
id: string;
service: string;
username: string;
} | null;
const selectClassName =
"flex h-10 w-full rounded-md border border-input bg-background px-3 text-body outline-none transition-colors placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50";
function CredentialDialogContent({
profileId,
credential,
onDone,
}: {
profileId: string;
credential: Credential;
onDone: () => void;
}) {
const isEdit = Boolean(credential);
const action = isEdit ? updateCredential : createCredential;
const [serviceId, setServiceId] = useState(
credential?.service ?? SERVICES[0].id
);
async function actionWithDoneCheck(_prevState: Awaited<ReturnType<typeof action>>, formData: FormData) {
const result = await action(_prevState, formData);
if (result && "success" in result && result.success) {
onDone();
}
return result;
}
const [state, formAction, pending] = useActionState(actionWithDoneCheck, undefined);
return (
<>
<DialogHeader className="mb-4">
<DialogTitle>{isEdit ? "Eintrag bearbeiten" : "Neuer Eintrag"}</DialogTitle>
</DialogHeader>
<form action={formAction}>
<input type="hidden" name="profileId" value={profileId} />
{isEdit && <input type="hidden" name="credentialId" value={credential!.id} />}
<FieldGroup>
<Field>
<FieldLabel htmlFor="service">
Dienst <span className="text-destructive">*</span>
</FieldLabel>
<div className="flex items-center gap-2">
<div className="flex size-10 shrink-0 items-center justify-center rounded-md border border-input bg-background">
<ServiceLogo serviceId={serviceId} size={20} className="size-5 object-contain" />
</div>
<select
id="service"
name="service"
value={serviceId}
onChange={(event) => setServiceId(event.target.value)}
required
className={selectClassName}
>
{SERVICES.map((service) => (
<option key={service.id} value={service.id}>
{service.label}
</option>
))}
</select>
</div>
</Field>
<Field>
<FieldLabel htmlFor="username">
Benutzername <span className="text-destructive">*</span>
</FieldLabel>
<Input
id="username"
name="username"
defaultValue={credential?.username ?? ""}
required
className="focus-visible:ring-0"
/>
</Field>
<Field>
<FieldLabel htmlFor="password">
Passwort {!isEdit && <span className="text-destructive">*</span>}
</FieldLabel>
<Input
id="password"
name="password"
type="password"
required={!isEdit}
placeholder={isEdit ? "Leer lassen, um Passwort zu behalten" : undefined}
className="focus-visible:ring-0"
/>
</Field>
{state && "error" in state && (
<p className="text-sm text-destructive">{state.error}</p>
)}
</FieldGroup>
<DialogFooter className="mt-6">
<Button type="submit" disabled={pending}>
{pending ? "Wird gespeichert..." : "Speichern"}
</Button>
</DialogFooter>
</form>
</>
);
}
export function CredentialDialog({
profileId,
credential,
trigger,
}: {
profileId: string;
credential?: Credential;
trigger?: React.ReactElement;
}) {
const [open, setOpen] = useState(false);
const [instanceKey, setInstanceKey] = useState(0);
function handleOpenChange(nextOpen: boolean) {
setOpen(nextOpen);
if (!nextOpen) {
setInstanceKey((key) => key + 1);
}
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger
render={trigger ?? <Button variant="outline" className="cursor-pointer" />}
>
{!trigger && (
<>
<PlusIcon className="size-4" />
Neuer Eintrag
</>
)}
</DialogTrigger>
<DialogContent>
<CredentialDialogContent
key={instanceKey}
profileId={profileId}
credential={credential ?? null}
onDone={() => handleOpenChange(false)}
/>
</DialogContent>
</Dialog>
);
}