3b94bceaf0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
220 lines
6.9 KiB
TypeScript
220 lines
6.9 KiB
TypeScript
"use client";
|
||
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { Loader2 } from "lucide-react";
|
||
import { useEffect, useState } from "react";
|
||
import { Controller, useForm } from "react-hook-form";
|
||
import { PasswordInput } from "@/components/password-input";
|
||
import { ResponsiveDialog } from "@/components/responsive-dialog";
|
||
import {
|
||
AlertDialog,
|
||
AlertDialogCancel,
|
||
AlertDialogContent,
|
||
AlertDialogDescription,
|
||
AlertDialogFooter,
|
||
AlertDialogHeader,
|
||
AlertDialogTitle,
|
||
} from "@/components/ui/alert-dialog";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { tr } from "@/i18n/tr";
|
||
import type { ActionResult } from "@/lib/action-result";
|
||
import {
|
||
banUserSchema,
|
||
setPasswordSchema,
|
||
type BanUserInput,
|
||
type SetPasswordInput,
|
||
} from "@/shared/schemas/user";
|
||
import type { AdminUser } from "../types";
|
||
|
||
const t = tr.adminUsers;
|
||
|
||
type DialogProps<T> = {
|
||
user: AdminUser | undefined;
|
||
open: boolean;
|
||
onOpenChange: (open: boolean) => void;
|
||
onConfirm: (user: AdminUser, values: T) => Promise<ActionResult>;
|
||
};
|
||
|
||
export function SetPasswordDialog({
|
||
user,
|
||
open,
|
||
onOpenChange,
|
||
onConfirm,
|
||
}: DialogProps<{ password: string }>) {
|
||
const form = useForm<SetPasswordInput>({
|
||
resolver: zodResolver(setPasswordSchema),
|
||
defaultValues: { password: "" },
|
||
});
|
||
useEffect(() => {
|
||
if (open) form.reset({ password: "" });
|
||
}, [open, form]);
|
||
|
||
async function onSubmit(values: SetPasswordInput) {
|
||
if (!user) return;
|
||
const result = await onConfirm(user, values);
|
||
if (result.ok) onOpenChange(false);
|
||
else form.setError("root", { message: result.error });
|
||
}
|
||
|
||
return (
|
||
<ResponsiveDialog
|
||
open={open}
|
||
onOpenChange={onOpenChange}
|
||
title={t.password.title}
|
||
description={user ? t.password.description(user.name) : undefined}
|
||
footer={
|
||
<>
|
||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||
{tr.common.cancel}
|
||
</Button>
|
||
<Button type="submit" form="password-form" disabled={form.formState.isSubmitting}>
|
||
{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : null}
|
||
{t.password.confirm}
|
||
</Button>
|
||
</>
|
||
}
|
||
>
|
||
<form id="password-form" onSubmit={form.handleSubmit(onSubmit)} noValidate>
|
||
<FieldGroup>
|
||
<Controller
|
||
name="password"
|
||
control={form.control}
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor="new-password">{t.password.newPassword}</FieldLabel>
|
||
<PasswordInput
|
||
{...field}
|
||
id="new-password"
|
||
autoComplete="new-password"
|
||
aria-invalid={fieldState.invalid}
|
||
aria-describedby="new-password-hint"
|
||
/>
|
||
<FieldDescription id="new-password-hint">{t.form.passwordHint}</FieldDescription>
|
||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||
</Field>
|
||
)}
|
||
/>
|
||
{form.formState.errors.root ? (
|
||
<FieldError>{form.formState.errors.root.message}</FieldError>
|
||
) : null}
|
||
</FieldGroup>
|
||
</form>
|
||
</ResponsiveDialog>
|
||
);
|
||
}
|
||
|
||
export function BanUserDialog({
|
||
user,
|
||
open,
|
||
onOpenChange,
|
||
onConfirm,
|
||
}: DialogProps<{ reason?: string }>) {
|
||
const form = useForm<BanUserInput, unknown, { reason?: string }>({
|
||
resolver: zodResolver(banUserSchema),
|
||
defaultValues: { reason: "" },
|
||
});
|
||
useEffect(() => {
|
||
if (open) form.reset({ reason: "" });
|
||
}, [open, form]);
|
||
|
||
async function onSubmit(values: { reason?: string }) {
|
||
if (!user) return;
|
||
const result = await onConfirm(user, values);
|
||
if (result.ok) onOpenChange(false);
|
||
else form.setError("root", { message: result.error });
|
||
}
|
||
|
||
return (
|
||
<ResponsiveDialog
|
||
open={open}
|
||
onOpenChange={onOpenChange}
|
||
title={t.ban.title}
|
||
description={user ? t.ban.description(user.name) : undefined}
|
||
footer={
|
||
<>
|
||
<Button variant="outline" type="button" onClick={() => onOpenChange(false)}>
|
||
{tr.common.cancel}
|
||
</Button>
|
||
<Button
|
||
type="submit"
|
||
form="ban-form"
|
||
variant="destructive"
|
||
disabled={form.formState.isSubmitting}
|
||
>
|
||
{form.formState.isSubmitting ? <Loader2 className="animate-spin" /> : null}
|
||
{t.ban.confirm}
|
||
</Button>
|
||
</>
|
||
}
|
||
>
|
||
<form id="ban-form" onSubmit={form.handleSubmit(onSubmit)} noValidate>
|
||
<FieldGroup>
|
||
<Controller
|
||
name="reason"
|
||
control={form.control}
|
||
render={({ field, fieldState }) => (
|
||
<Field data-invalid={fieldState.invalid}>
|
||
<FieldLabel htmlFor="ban-reason">{t.ban.reason}</FieldLabel>
|
||
<Textarea
|
||
{...field}
|
||
value={field.value ?? ""}
|
||
id="ban-reason"
|
||
rows={3}
|
||
aria-invalid={fieldState.invalid}
|
||
/>
|
||
{fieldState.invalid ? <FieldError errors={[fieldState.error]} /> : null}
|
||
</Field>
|
||
)}
|
||
/>
|
||
{form.formState.errors.root ? (
|
||
<FieldError>{form.formState.errors.root.message}</FieldError>
|
||
) : null}
|
||
</FieldGroup>
|
||
</form>
|
||
</ResponsiveDialog>
|
||
);
|
||
}
|
||
|
||
export function DeleteUserDialog({ user, open, onOpenChange, onConfirm }: DialogProps<void>) {
|
||
const [pending, setPending] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
function handleOpenChange(next: boolean) {
|
||
if (!next) setError(null);
|
||
onOpenChange(next);
|
||
}
|
||
|
||
async function confirm() {
|
||
if (!user) return;
|
||
setPending(true);
|
||
const result = await onConfirm(user, undefined);
|
||
setPending(false);
|
||
if (result.ok) handleOpenChange(false);
|
||
else setError(result.error);
|
||
}
|
||
|
||
return (
|
||
<AlertDialog open={open} onOpenChange={handleOpenChange}>
|
||
<AlertDialogContent>
|
||
<AlertDialogHeader>
|
||
<AlertDialogTitle>{t.remove.title}</AlertDialogTitle>
|
||
<AlertDialogDescription>
|
||
{user ? t.remove.description(user.name) : null}
|
||
</AlertDialogDescription>
|
||
</AlertDialogHeader>
|
||
{error ? <FieldError>{error}</FieldError> : null}
|
||
<AlertDialogFooter>
|
||
<AlertDialogCancel disabled={pending}>{tr.common.cancel}</AlertDialogCancel>
|
||
{/* AlertDialogAction otomatik kapanır; sonuç beklenmesi için normal düğme. */}
|
||
<Button variant="destructive" onClick={confirm} disabled={pending}>
|
||
{pending ? <Loader2 className="animate-spin" /> : null}
|
||
{t.remove.confirm}
|
||
</Button>
|
||
</AlertDialogFooter>
|
||
</AlertDialogContent>
|
||
</AlertDialog>
|
||
);
|
||
}
|