feat(admin): kullanıcı yönetimi tablosu ve diyalogları (mock)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user